From 69013392d3ef7c0f1dee0c6e4a8d626866e96223 Mon Sep 17 00:00:00 2001 From: sergeych Date: Tue, 7 Apr 2026 19:51:04 +0300 Subject: [PATCH] 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 --- .../kotlin/net/sergeych/lyng/LegacyDigest.kt | 124 ++++++++++++++++++ .../kotlin/net/sergeych/lyng/Script.kt | 14 ++ .../net/sergeych/lyng/LegacyDigestTest.kt | 113 ++++++++++++++++ lynglib/stdlib/lyng/legacy_digest.lyng | 33 +++++ 4 files changed, 284 insertions(+) create mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/LegacyDigest.kt create mode 100644 lynglib/src/commonTest/kotlin/net/sergeych/lyng/LegacyDigestTest.kt create mode 100644 lynglib/stdlib/lyng/legacy_digest.lyng diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/LegacyDigest.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/LegacyDigest.kt new file mode 100644 index 0000000..693db31 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/LegacyDigest.kt @@ -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) + } + } +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt index 50cc0d7..06a936b 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt @@ -29,6 +29,7 @@ import net.sergeych.lyng.obj.* import net.sergeych.lyng.pacman.ImportManager import net.sergeych.lyng.stdlib_included.complexLyng 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.observableLyng import net.sergeych.lyng.stdlib_included.operatorsLyng @@ -896,6 +897,19 @@ class Script( module.eval(Source("lyng.complex", complexLyng)) ObjComplexSupport.bindTo(module) } + addPackage("lyng.legacy_digest") { module -> + module.eval(Source("lyng.legacy_digest", legacyDigestLyng)) + module.bindObject("LegacyDigest") { + addFun("sha1") { + val data = requiredArg(0) + val bytes = when (data) { + is ObjBuffer -> data.byteArray.toByteArray() + else -> data.toString().encodeToByteArray() + } + ObjString(LegacyDigest.sha1Hex(bytes)) + } + } + } addPackage("lyng.buffer") { it.addConstDoc( name = "Buffer", diff --git a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/LegacyDigestTest.kt b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/LegacyDigestTest.kt new file mode 100644 index 0000000..4e67344 --- /dev/null +++ b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/LegacyDigestTest.kt @@ -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() + ) + } +} diff --git a/lynglib/stdlib/lyng/legacy_digest.lyng b/lynglib/stdlib/lyng/legacy_digest.lyng new file mode 100644 index 0000000..5c8b463 --- /dev/null +++ b/lynglib/stdlib/lyng/legacy_digest.lyng @@ -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 +}