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:
Sergey Chernov 2026-04-07 19:51:04 +03:00
parent f145a90845
commit 69013392d3
4 changed files with 284 additions and 0 deletions

View 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)
}
}
}

View File

@ -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",

View File

@ -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()
)
}
}

View 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
}