Add lyng.legacy_digest module with LegacyDigest.sha1()
Provides a pure Kotlin/KMP SHA-1 implementation with no extra dependencies,
exposed as `LegacyDigest.sha1(data)` in the `lyng.legacy_digest` package.
The naming deliberately signals that SHA-1 is cryptographically broken:
the object name `LegacyDigest` and prominent doc-comment warnings steer
users away from security-sensitive use while still enabling interoperability
with legacy protocols and file formats that require SHA-1.
API:
import lyng.legacy_digest
val hex: String = LegacyDigest.sha1("some string") // UTF-8 input
val hex: String = LegacyDigest.sha1(someBuffer) // raw-byte input
Tests cover FIPS 180-4 known-answer vectors (empty, "abc", long message,
quick-brown-fox) at both the Kotlin and Lyng integration levels.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f145a90845
commit
69013392d3
124
lynglib/src/commonMain/kotlin/net/sergeych/lyng/LegacyDigest.kt
Normal file
124
lynglib/src/commonMain/kotlin/net/sergeych/lyng/LegacyDigest.kt
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.sergeych.lyng
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure Kotlin/KMP implementation of legacy hash functions.
|
||||||
|
*
|
||||||
|
* SHA-1 is cryptographically broken and must not be used for security-sensitive
|
||||||
|
* purposes (password hashing, digital signatures, etc.). It is retained here
|
||||||
|
* solely for compatibility with legacy protocols and file formats that require it.
|
||||||
|
*/
|
||||||
|
internal object LegacyDigest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the SHA-1 digest of [input] and return it as a lowercase hex string.
|
||||||
|
*
|
||||||
|
* SHA-1 is **cryptographically insecure**. Use only for protocol compatibility.
|
||||||
|
*/
|
||||||
|
fun sha1Hex(input: ByteArray): String {
|
||||||
|
val digest = sha1(input)
|
||||||
|
return buildString(40) {
|
||||||
|
for (b in digest) {
|
||||||
|
val v = b.toInt() and 0xFF
|
||||||
|
if (v < 16) append('0')
|
||||||
|
append(v.toString(16))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sha1(input: ByteArray): ByteArray {
|
||||||
|
// Initial hash values
|
||||||
|
var h0 = 0x67452301
|
||||||
|
var h1 = 0xEFCDAB89.toInt()
|
||||||
|
var h2 = 0x98BADCFE.toInt()
|
||||||
|
var h3 = 0x10325476
|
||||||
|
var h4 = 0xC3D2E1F0.toInt()
|
||||||
|
|
||||||
|
// Pre-processing: pad message to a multiple of 512 bits (64 bytes).
|
||||||
|
// Append 0x80, then zeros, then the 64-bit big-endian bit-length.
|
||||||
|
val msgLen = input.size
|
||||||
|
val bitLen = msgLen.toLong() * 8L
|
||||||
|
// Minimum padding: 1 byte (0x80) + 8 bytes (length) = 9 bytes.
|
||||||
|
// Total length must be ≡ 0 (mod 64).
|
||||||
|
val padded = run {
|
||||||
|
val totalLen = ((msgLen + 1 + 8 + 63) / 64) * 64
|
||||||
|
ByteArray(totalLen).also { buf ->
|
||||||
|
input.copyInto(buf)
|
||||||
|
buf[msgLen] = 0x80.toByte()
|
||||||
|
// Big-endian 64-bit bit-length in the last 8 bytes
|
||||||
|
for (i in 0..7) {
|
||||||
|
buf[totalLen - 8 + i] = ((bitLen ushr (56 - i * 8)) and 0xFF).toByte()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val w = IntArray(80)
|
||||||
|
|
||||||
|
var blockStart = 0
|
||||||
|
while (blockStart < padded.size) {
|
||||||
|
// Build the 80-word message schedule
|
||||||
|
for (i in 0..15) {
|
||||||
|
val off = blockStart + i * 4
|
||||||
|
w[i] = ((padded[off].toInt() and 0xFF) shl 24) or
|
||||||
|
((padded[off + 1].toInt() and 0xFF) shl 16) or
|
||||||
|
((padded[off + 2].toInt() and 0xFF) shl 8) or
|
||||||
|
(padded[off + 3].toInt() and 0xFF)
|
||||||
|
}
|
||||||
|
for (i in 16..79) {
|
||||||
|
val x = w[i - 3] xor w[i - 8] xor w[i - 14] xor w[i - 16]
|
||||||
|
w[i] = (x shl 1) or (x ushr 31) // ROTL-1
|
||||||
|
}
|
||||||
|
|
||||||
|
var a = h0; var b = h1; var c = h2; var d = h3; var e = h4
|
||||||
|
|
||||||
|
for (t in 0..19) {
|
||||||
|
val f = (b and c) or (b.inv() and d)
|
||||||
|
val temp = ((a shl 5) or (a ushr 27)) + f + e + 0x5A827999 + w[t]
|
||||||
|
e = d; d = c; c = (b shl 30) or (b ushr 2); b = a; a = temp
|
||||||
|
}
|
||||||
|
for (t in 20..39) {
|
||||||
|
val f = b xor c xor d
|
||||||
|
val temp = ((a shl 5) or (a ushr 27)) + f + e + 0x6ED9EBA1 + w[t]
|
||||||
|
e = d; d = c; c = (b shl 30) or (b ushr 2); b = a; a = temp
|
||||||
|
}
|
||||||
|
for (t in 40..59) {
|
||||||
|
val f = (b and c) or (b and d) or (c and d)
|
||||||
|
val temp = ((a shl 5) or (a ushr 27)) + f + e + 0x8F1BBCDC.toInt() + w[t]
|
||||||
|
e = d; d = c; c = (b shl 30) or (b ushr 2); b = a; a = temp
|
||||||
|
}
|
||||||
|
for (t in 60..79) {
|
||||||
|
val f = b xor c xor d
|
||||||
|
val temp = ((a shl 5) or (a ushr 27)) + f + e + 0xCA62C1D6.toInt() + w[t]
|
||||||
|
e = d; d = c; c = (b shl 30) or (b ushr 2); b = a; a = temp
|
||||||
|
}
|
||||||
|
|
||||||
|
h0 += a; h1 += b; h2 += c; h3 += d; h4 += e
|
||||||
|
blockStart += 64
|
||||||
|
}
|
||||||
|
|
||||||
|
return ByteArray(20).also { out ->
|
||||||
|
fun putInt(off: Int, v: Int) {
|
||||||
|
out[off] = (v ushr 24).toByte()
|
||||||
|
out[off + 1] = (v ushr 16).toByte()
|
||||||
|
out[off + 2] = (v ushr 8).toByte()
|
||||||
|
out[off + 3] = v.toByte()
|
||||||
|
}
|
||||||
|
putInt(0, h0); putInt(4, h1); putInt(8, h2); putInt(12, h3); putInt(16, h4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -29,6 +29,7 @@ import net.sergeych.lyng.obj.*
|
|||||||
import net.sergeych.lyng.pacman.ImportManager
|
import net.sergeych.lyng.pacman.ImportManager
|
||||||
import net.sergeych.lyng.stdlib_included.complexLyng
|
import net.sergeych.lyng.stdlib_included.complexLyng
|
||||||
import net.sergeych.lyng.stdlib_included.decimalLyng
|
import net.sergeych.lyng.stdlib_included.decimalLyng
|
||||||
|
import net.sergeych.lyng.stdlib_included.legacyDigestLyng
|
||||||
import net.sergeych.lyng.stdlib_included.matrixLyng
|
import net.sergeych.lyng.stdlib_included.matrixLyng
|
||||||
import net.sergeych.lyng.stdlib_included.observableLyng
|
import net.sergeych.lyng.stdlib_included.observableLyng
|
||||||
import net.sergeych.lyng.stdlib_included.operatorsLyng
|
import net.sergeych.lyng.stdlib_included.operatorsLyng
|
||||||
@ -896,6 +897,19 @@ class Script(
|
|||||||
module.eval(Source("lyng.complex", complexLyng))
|
module.eval(Source("lyng.complex", complexLyng))
|
||||||
ObjComplexSupport.bindTo(module)
|
ObjComplexSupport.bindTo(module)
|
||||||
}
|
}
|
||||||
|
addPackage("lyng.legacy_digest") { module ->
|
||||||
|
module.eval(Source("lyng.legacy_digest", legacyDigestLyng))
|
||||||
|
module.bindObject("LegacyDigest") {
|
||||||
|
addFun("sha1") {
|
||||||
|
val data = requiredArg<Obj>(0)
|
||||||
|
val bytes = when (data) {
|
||||||
|
is ObjBuffer -> data.byteArray.toByteArray()
|
||||||
|
else -> data.toString().encodeToByteArray()
|
||||||
|
}
|
||||||
|
ObjString(LegacyDigest.sha1Hex(bytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
addPackage("lyng.buffer") {
|
addPackage("lyng.buffer") {
|
||||||
it.addConstDoc(
|
it.addConstDoc(
|
||||||
name = "Buffer",
|
name = "Buffer",
|
||||||
|
|||||||
@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.sergeych.lyng
|
||||||
|
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class LegacyDigestTest {
|
||||||
|
|
||||||
|
// --- Kotlin-level unit tests for the SHA-1 implementation ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sha1KotlinEmptyString() {
|
||||||
|
// SHA-1("") = da39a3ee5e6b4b0d3255bfef95601890afd80709
|
||||||
|
assertEquals(
|
||||||
|
"da39a3ee5e6b4b0d3255bfef95601890afd80709",
|
||||||
|
LegacyDigest.sha1Hex(ByteArray(0))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sha1KotlinAbc() {
|
||||||
|
// SHA-1("abc") = a9993e364706816aba3e25717850c26c9cd0d89d
|
||||||
|
assertEquals(
|
||||||
|
"a9993e364706816aba3e25717850c26c9cd0d89d",
|
||||||
|
LegacyDigest.sha1Hex("abc".encodeToByteArray())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sha1KotlinLongerMessage() {
|
||||||
|
// SHA-1("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq")
|
||||||
|
// = 84983e441c3bd26ebaae4aa1f95129e5e54670f1
|
||||||
|
val msg = "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"
|
||||||
|
assertEquals(
|
||||||
|
"84983e441c3bd26ebaae4aa1f95129e5e54670f1",
|
||||||
|
LegacyDigest.sha1Hex(msg.encodeToByteArray())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sha1KotlinExactlyOneBlock() {
|
||||||
|
// "The quick brown fox jumps over the lazy dog"
|
||||||
|
// SHA-1 = 2fd4e1c67a2d28fced849ee1bb76e7391b93eb12
|
||||||
|
val msg = "The quick brown fox jumps over the lazy dog"
|
||||||
|
assertEquals(
|
||||||
|
"2fd4e1c67a2d28fced849ee1bb76e7391b93eb12",
|
||||||
|
LegacyDigest.sha1Hex(msg.encodeToByteArray())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Lyng-level integration tests ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sha1LyngStringInput() = runTest {
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
import lyng.legacy_digest
|
||||||
|
assertEquals(
|
||||||
|
"a9993e364706816aba3e25717850c26c9cd0d89d",
|
||||||
|
LegacyDigest.sha1("abc")
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"da39a3ee5e6b4b0d3255bfef95601890afd80709",
|
||||||
|
LegacyDigest.sha1("")
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sha1LyngBufferInput() = runTest {
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
import lyng.legacy_digest
|
||||||
|
import lyng.buffer
|
||||||
|
val buf = Buffer.decodeHex("616263") // "abc" in hex
|
||||||
|
assertEquals(
|
||||||
|
"a9993e364706816aba3e25717850c26c9cd0d89d",
|
||||||
|
LegacyDigest.sha1(buf)
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sha1LyngReturnType() = runTest {
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
import lyng.legacy_digest
|
||||||
|
val h = LegacyDigest.sha1("hello")
|
||||||
|
assert(h is String)
|
||||||
|
assertEquals(40, h.length)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
33
lynglib/stdlib/lyng/legacy_digest.lyng
Normal file
33
lynglib/stdlib/lyng/legacy_digest.lyng
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package lyng.legacy_digest
|
||||||
|
|
||||||
|
/*
|
||||||
|
Legacy cryptographic digest functions.
|
||||||
|
|
||||||
|
⚠️ WARNING: The functions in this module use algorithms that are
|
||||||
|
CRYPTOGRAPHICALLY BROKEN and must NOT be used for any security-sensitive
|
||||||
|
purpose, including:
|
||||||
|
- Password hashing
|
||||||
|
- Digital signatures or MACs
|
||||||
|
- Integrity verification against adversarial tampering
|
||||||
|
|
||||||
|
Use only for interoperability with legacy protocols, file formats, or
|
||||||
|
external systems that require these specific hash values.
|
||||||
|
|
||||||
|
For secure hashing, use SHA-256 or better (not yet available in this stdlib).
|
||||||
|
*/
|
||||||
|
|
||||||
|
extern object LegacyDigest {
|
||||||
|
/*
|
||||||
|
Compute the SHA-1 digest of `data` and return it as a lowercase hex string.
|
||||||
|
|
||||||
|
`data` may be a String (hashed as UTF-8) or a Buffer (hashed as raw bytes).
|
||||||
|
|
||||||
|
⚠️ SHA-1 is cryptographically broken. Use only for protocol compatibility.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
import lyng.legacy_digest
|
||||||
|
val digest = LegacyDigest.sha1("hello world")
|
||||||
|
// "2aae6c69a64d7c8b96eacbbe0ced5df20f3a8e89" (note: known SHA-1 of "hello world")
|
||||||
|
*/
|
||||||
|
extern fun sha1(data): String
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user