Skip to content

Commit a412bc9

Browse files
committed
fix(model-client): sha256 computations were wrong in Chrome
Replaced it js-sha256 with @aws-crypto/sha256-js. Also added consistency checks on server and client side to ensure both side compute the same hash value. see emn178/js-sha256#40
1 parent c210e8f commit a412bc9

File tree

16 files changed

+189
-194
lines changed

16 files changed

+189
-194
lines changed

kotlin-js-store/yarn.lock

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,39 @@
22
# yarn lockfile v1
33

44

5+
"@aws-crypto/sha256-js@^5.0.0":
6+
version "5.0.0"
7+
resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-5.0.0.tgz#fec6d5a9a097e812207eacaaa707bfa9191b3ad8"
8+
integrity sha512-g+u9iKkaQVp9Mjoxq1IJSHj9NHGZF441+R/GIH0dn7u4mix5QQ4VqgpppHrNm1LzjUzb0BpcFGsBXP6cOVf+ZQ==
9+
dependencies:
10+
"@aws-crypto/util" "^5.0.0"
11+
"@aws-sdk/types" "^3.222.0"
12+
tslib "^1.11.1"
13+
14+
"@aws-crypto/util@^5.0.0":
15+
version "5.0.0"
16+
resolved "https://registry.yarnpkg.com/@aws-crypto/util/-/util-5.0.0.tgz#afa286af897ea2bd9fab194b4a6be9cc562db23a"
17+
integrity sha512-1GYqLdYRe96idcCltlqxdJ68OWE6ADT8qGLmVi7PVHKl8AxD2EWSbJSSevPq2eTx6vaPZpkr1RoZ3lcw/uGoEA==
18+
dependencies:
19+
"@aws-sdk/types" "^3.222.0"
20+
"@aws-sdk/util-utf8-browser" "^3.0.0"
21+
tslib "^1.11.1"
22+
23+
"@aws-sdk/types@^3.222.0":
24+
version "3.398.0"
25+
resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.398.0.tgz#8ce02559536670f9188cddfce32e9dd12b4fe965"
26+
integrity sha512-r44fkS+vsEgKCuEuTV+TIk0t0m5ZlXHNjSDYEUvzLStbbfUFiNus/YG4UCa0wOk9R7VuQI67badsvvPeVPCGDQ==
27+
dependencies:
28+
"@smithy/types" "^2.2.2"
29+
tslib "^2.5.0"
30+
31+
"@aws-sdk/util-utf8-browser@^3.0.0":
32+
version "3.259.0"
33+
resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz#3275a6f5eb334f96ca76635b961d3c50259fd9ff"
34+
integrity sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==
35+
dependencies:
36+
tslib "^2.3.1"
37+
538
639
version "1.5.0"
740
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
@@ -78,6 +111,13 @@
78111
"@modelix/ts-model-api@file:../../ts-model-api":
79112
version "1.3.2-kernelf.4.dirty-SNAPSHOT"
80113

114+
"@smithy/types@^2.2.2":
115+
version "2.2.2"
116+
resolved "https://registry.yarnpkg.com/@smithy/types/-/types-2.2.2.tgz#bd8691eb92dd07ac33b83e0e1c45f283502b1bf7"
117+
integrity sha512-4PS0y1VxDnELGHGgBWlDksB2LJK8TG8lcvlWxIsgR+8vROI7Ms8h1P4FQUx+ftAX2QZv5g1CJCdhdRmQKyonyw==
118+
dependencies:
119+
tslib "^2.5.0"
120+
81121
"@socket.io/component-emitter@~3.1.0":
82122
version "3.1.0"
83123
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
@@ -2045,6 +2085,16 @@ tr46@~0.0.3:
20452085
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
20462086
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
20472087

2088+
tslib@^1.11.1:
2089+
version "1.14.1"
2090+
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
2091+
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
2092+
2093+
tslib@^2.3.1, tslib@^2.5.0:
2094+
version "2.6.2"
2095+
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
2096+
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
2097+
20482098
type-check@~0.3.2:
20492099
version "0.3.2"
20502100
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"

model-client/build.gradle.kts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@ val kotlinxSerializationVersion: String by rootProject
2020
kotlin {
2121
jvm()
2222
js(IR) {
23-
// browser {}
23+
browser {
24+
testTask {
25+
useMocha {
26+
timeout = "30s"
27+
}
28+
}
29+
}
2430
nodejs {
2531
testTask {
2632
useMocha {

model-client/src/commonMain/kotlin/org/modelix/model/client2/ModelClientV2.kt

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,14 +134,15 @@ class ModelClientV2(
134134
require(version is CLVersion)
135135
require(baseVersion is CLVersion?)
136136
version.write()
137-
val objects = version.computeDelta(baseVersion).values.filterNotNull().toSet()
137+
val objects = version.computeDelta(baseVersion)
138138
val response = httpClient.post {
139139
url {
140140
takeFrom(baseUrl)
141141
appendPathSegments("repositories", branch.repositoryId.id, "branches", branch.branchName)
142142
}
143143
contentType(ContentType.Application.Json)
144-
val body = VersionDelta(version.getContentHash(), null, objects)
144+
val body = VersionDelta(version.getContentHash(), null, objectsMap = objects)
145+
body.checkObjectHashes()
145146
setBody(body)
146147
}
147148
val mergedVersionDelta = response.body<VersionDelta>()
@@ -192,13 +193,13 @@ class ModelClientV2(
192193
return if (baseVersion == null) {
193194
CLVersion(
194195
delta.versionHash,
195-
store.also { it.keyValueStore.putAll(delta.objects.associateBy { HashUtil.sha256(it) }) },
196+
store.also { it.keyValueStore.putAll(delta.getAllObjects()) },
196197
)
197198
} else if (delta.versionHash == baseVersion.hash) {
198199
baseVersion
199200
} else {
200201
require(baseVersion.store == store) { "baseVersion was not created by this client" }
201-
store.keyValueStore.putAll(delta.objects.associateBy { HashUtil.sha256(it) })
202+
store.keyValueStore.putAll(delta.getAllObjects())
202203
CLVersion(
203204
delta.versionHash,
204205
baseVersion.store,
@@ -275,3 +276,9 @@ abstract class ModelClientV2Builder {
275276
}
276277

277278
expect class ModelClientV2PlatformSpecificBuilder() : ModelClientV2Builder
279+
280+
fun VersionDelta.checkObjectHashes() {
281+
HashUtil.checkObjectHashes(objectsMap)
282+
}
283+
284+
fun VersionDelta.getAllObjects(): Map<String, String> = objectsMap + objects.associateBy { HashUtil.sha256(it) }

model-client/src/commonTest/kotlin/org/modelix/model/HashUtilsTest.kt

Lines changed: 0 additions & 65 deletions
This file was deleted.

model-client/src/commonTest/kotlin/org/modelix/model/StringUtilsTest.kt

Lines changed: 0 additions & 30 deletions
This file was deleted.

model-datastructure/build.gradle.kts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ plugins {
66
kotlin {
77
jvm()
88
js(IR) {
9-
// browser {}
9+
browser {}
1010
nodejs {
1111
testTask {
1212
useMocha {
@@ -34,8 +34,8 @@ kotlin {
3434
}
3535
val commonTest by getting {
3636
dependencies {
37-
// kotlin("test-common")
38-
// kotlin("test-annotations-common")
37+
kotlin("test-common")
38+
kotlin("test-annotations-common")
3939
}
4040
}
4141
val jvmMain by getting {
@@ -62,20 +62,21 @@ kotlin {
6262
}
6363
val jvmTest by getting {
6464
dependencies {
65-
// implementation(kotlin("test"))
65+
implementation(kotlin("test"))
6666
}
6767
}
6868
val jsMain by getting {
6969
dependencies {
7070
// implementation(kotlin("stdlib-js"))
7171
// implementation(npm("uuid", "^8.3.0"))
7272
// implementation(npm("js-sha256", "^0.9.0"))
73+
implementation(npm("@aws-crypto/sha256-js", "^5.0.0"))
7374
// implementation(npm("js-base64", "^3.4.5"))
7475
}
7576
}
7677
val jsTest by getting {
7778
dependencies {
78-
// implementation(kotlin("test-js"))
79+
implementation(kotlin("test-js"))
7980
}
8081
}
8182
}

model-datastructure/src/commonMain/kotlin/org/modelix/model/lazy/CLVersion.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -285,10 +285,13 @@ class CLVersion : IVersion {
285285
}
286286
}
287287

288-
fun CLVersion.computeDelta(baseVersion: CLVersion?): Map<String, String?> {
289-
return computeDelta(store.keyValueStore, this.getContentHash(), baseVersion?.getContentHash())
288+
fun CLVersion.computeDelta(baseVersion: CLVersion?): Map<String, String> {
289+
return computeDelta(store.keyValueStore, this.getContentHash(), baseVersion?.getContentHash()).filterNotNullValues()
290290
}
291291

292+
@Suppress("UNCHECKED_CAST")
293+
fun <K, V> Map<K, V?>.filterNotNullValues(): Map<K, V> = filterValues { it != null } as Map<K, V>
294+
292295
private fun computeDelta(keyValueStore: IKeyValueStore, versionHash: String, baseVersionHash: String?): Map<String, String?> {
293296
val changedNodeIds = HashSet<Long>()
294297
val oldAndNewEntries: Map<String, String?> = trackAccessedEntries(keyValueStore) { store ->

model-datastructure/src/commonMain/kotlin/org/modelix/model/persistent/HashUtil.kt

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@ package org.modelix.model.persistent
1818
object HashUtil {
1919
val HASH_PATTERN = Regex("""[a-zA-Z0-9\-_]{5}\*[a-zA-Z0-9\-_]{38}""")
2020

21-
fun sha256asByteArray(input: ByteArray?): ByteArray = PlatformSpecificHashUtil.sha256asByteArray(input)
22-
fun sha256(input: ByteArray?): String = PlatformSpecificHashUtil.sha256(input)
23-
fun sha256(input: String): String = PlatformSpecificHashUtil.sha256(input)
21+
fun sha256asByteArray(input: String): ByteArray = PlatformSpecificHashUtil.sha256asByteArray(input)
22+
23+
fun sha256(input: String): String {
24+
val base64 = PlatformSpecificHashUtil.base64encode(sha256asByteArray(input))
25+
return base64.substring(0, 5) + "*" + base64.substring(5)
26+
}
2427

2528
fun isSha256(value: String?): Boolean {
2629
return if (value == null || value.length != 44) {
@@ -35,8 +38,16 @@ object HashUtil {
3538
return HASH_PATTERN.findAll(input).map { it.groupValues.first() }.asIterable()
3639
}
3740

38-
fun base64encode(input: String): String = PlatformSpecificHashUtil.base64encode(input)
39-
fun base64encode(input: ByteArray): String = PlatformSpecificHashUtil.base64encode(input)
40-
fun base64decode(input: String): String = PlatformSpecificHashUtil.base64decode(input)
41-
fun stringToUTF8ByteArray(input: String): ByteArray = PlatformSpecificHashUtil.stringToUTF8ByteArray(input)
41+
fun checkObjectHashes(entries: Map<String, String?>) {
42+
for (entry in entries) {
43+
val value = entry.value ?: continue
44+
if (!isSha256(entry.key)) continue
45+
val computedHash = sha256(value)
46+
val providedHash = entry.key
47+
require(computedHash == providedHash) {
48+
val bytes = value.encodeToByteArray(throwOnInvalidSequence = true)
49+
"Provided hash $providedHash doesn't match the computed hash $computedHash for value: $value\n Value as ByteArray$bytes"
50+
}
51+
}
52+
}
4253
}
Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
package org.modelix.model.persistent
22

33
expect object PlatformSpecificHashUtil {
4-
fun sha256asByteArray(input: ByteArray?): ByteArray
5-
fun sha256(input: ByteArray?): String
6-
fun sha256(input: String): String
7-
fun base64encode(input: String): String
4+
fun sha256asByteArray(input: String): ByteArray
85
fun base64encode(input: ByteArray): String
9-
fun base64decode(input: String): String
10-
fun stringToUTF8ByteArray(input: String): ByteArray
116
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright (c) 2023.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import org.modelix.model.persistent.HashUtil
18+
import kotlin.test.Test
19+
import kotlin.test.assertEquals
20+
21+
class HashUtilsTest {
22+
23+
@Test
24+
fun testKnownIssue1() {
25+
val res = HashUtil.sha256("courses/2/Xsmvf*PD7Sfna8J23fw4VWAT-g9KiXvRfUjYgCYGkZP8")
26+
assertEquals("hgopc*4n9Ddc5-Di5f9i0gXA_LsGwuBN9ir-fmJTsvp4", res)
27+
}
28+
29+
@Test
30+
fun testKnownIssue2() {
31+
val res = HashUtil.sha256("100000016/mps%3A96533389-8d4c-46f2-b150-8d89155f7fca%2F4128798754188010580/100000015/rooms/100000017,100000018,100000019/%23mpsNodeId%23=5042850610501964316,maximumCapacity=30,name=Marie,number=1.311/")
32+
assertEquals("wViE7*Dr2QLdMzP6YhAFA-8aKgx3USS1butvAODA22y0", res)
33+
}
34+
35+
@Test
36+
fun testKnownIssue4() {
37+
val input = "L/10000002a/hCo9C*azyKaBGRwpGmcmCzK4zNpVvpJtdvaNh_Ai7Ljo"
38+
val res = HashUtil.sha256(input)
39+
assertEquals("CgU1o*tzhHi3xPr2AA-ucivjYQ2qx0fzC9V8ZIa9w0UM", res)
40+
}
41+
42+
@Test
43+
fun unicodeString() {
44+
val res = HashUtil.sha256("⊣ⵊⰵ₪┨₩⛎⋪⯏⋂⇤⅐\u244F⪶⸎⡚⚅⑼➐≘⍗⚬☄⦍≧⯢⻱\u2029\u20C3⒋Ⱍ⑊⨩ⱡ⡉⩓⽄◳┸⇅⻖⸙⍹\u200B⯴⺁⎾ℤ\u2432␞⊘╻⼒")
45+
assertEquals("_KlSg*f0Y4i8Z8GRd672y1SoNwHPm22crM-NYrYUdIiQ", res)
46+
}
47+
48+
@Test
49+
fun testSha256EmptyString() {
50+
val res = HashUtil.sha256("")
51+
assertEquals("47DEQ*pj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU", res)
52+
}
53+
54+
@Test
55+
fun testSha256AsciiString() {
56+
val res = HashUtil.sha256("""!"#${'$'}%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~""")
57+
assertEquals("TlD0a*IifAn-A7RlySVGi9Xb6YeBMJ9vp7JiPUGtZH-U", res)
58+
}
59+
}

0 commit comments

Comments
 (0)