From 3f69cab11af64a45e6f0d0affdc5c74e2c93f276 Mon Sep 17 00:00:00 2001 From: makkarpov Date: Sun, 19 Jul 2015 14:04:17 +0300 Subject: [PATCH] Add tiered data cards: - Tier 1: Basic data card operations - Tier 2: Basic operations + AES-CBC encryption/decryption - Tier 3: Basic + AES-CBC + ECDSA + ECDH - Textures are now looking like a stubs that are waiting for better alternatives - Recipes by Kubuxu - gpg.lua program (simple version without keychain) --- Change log after squash --- + Changed to ThreadLocal instance of SecureRandom instead of genSeed function + Added readable errors in case of invalid key type + Added SecureRandom instance to EC key generator to avoid unnecessary creation of another one. + Added check that data card is present and `data.present()` function + Fixed typo at https://github.com/MightyPirates/OpenComputers/blob/029915d5956b1ac69570fec4e1d4b64f4aab9cac/src/main/resources/assets/opencomputers/lua/component/data/lib/data.lua#L15 --- build.gradle | 5 +- src/main/resources/application.conf | 25 +- .../opencomputers/doc/ru_RU/item/dataCard.md | 19 +- .../assets/opencomputers/lang/de_DE.lang | 8 +- .../assets/opencomputers/lang/en_US.lang | 8 +- .../assets/opencomputers/lang/ru_RU.lang | 4 +- .../lua/component/data/bin/gpg.lua | 241 ++++++++++++ .../lua/component/data/lib/data.lua | 48 +-- .../opencomputers/recipes/default.recipes | 8 + .../items/{DataCard.png => DataCard0.png} | Bin ...taCard.png.mcmeta => DataCard0.png.mcmeta} | 0 .../textures/items/DataCard1.png | Bin 0 -> 749 bytes .../textures/items/DataCard1.png.mcmeta | 15 + .../textures/items/DataCard2.png | Bin 0 -> 710 bytes .../textures/items/DataCard2.png.mcmeta | 15 + src/main/scala/li/cil/oc/Constants.scala | 4 +- src/main/scala/li/cil/oc/Settings.scala | 5 + .../scala/li/cil/oc/common/init/Items.scala | 6 +- .../li/cil/oc/common/item/DataCard.scala | 6 +- .../opencomputers/DriverDataCard.scala | 22 +- .../li/cil/oc/server/component/DataCard.scala | 343 ++++++++++++++---- 21 files changed, 657 insertions(+), 125 deletions(-) create mode 100644 src/main/resources/assets/opencomputers/lua/component/data/bin/gpg.lua rename src/main/resources/assets/opencomputers/textures/items/{DataCard.png => DataCard0.png} (100%) rename src/main/resources/assets/opencomputers/textures/items/{DataCard.png.mcmeta => DataCard0.png.mcmeta} (100%) create mode 100644 src/main/resources/assets/opencomputers/textures/items/DataCard1.png create mode 100644 src/main/resources/assets/opencomputers/textures/items/DataCard1.png.mcmeta create mode 100644 src/main/resources/assets/opencomputers/textures/items/DataCard2.png create mode 100644 src/main/resources/assets/opencomputers/textures/items/DataCard2.png.mcmeta diff --git a/build.gradle b/build.gradle index 52beff92c..1e2fb9f78 100644 --- a/build.gradle +++ b/build.gradle @@ -182,7 +182,10 @@ dependencies { provided "codechicken:WR-CBE:${config.minecraft.version}-${config.wrcbe.version}:dev" provided "com.bluepowermod:BluePower:${config.bluepower.version}:deobf" provided "com.gregoriust.gregtech:gregtech_${config.minecraft.version}:${config.gt.version}:dev" - provided "dev.calclavia.resonantengine:resonant-engine:${config.re.version}:dev" + + // Fails to download and OC doesn't seems to have integration module for it. + //provided "dev.calclavia.resonantengine:resonant-engine:${config.re.version}:dev" + provided "igwmod:IGW-Mod-1.7.10:${config.igwmod.version}:userdev" provided "mcp.mobius.waila:Waila:${config.waila.version}_${config.minecraft.version}:dev" provided "net.industrial-craft:industrialcraft-2:${config.ic2.version}:dev" diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index ffb89d9c8..6639f11ee 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -747,11 +747,28 @@ opencomputers { # moving very slowly this also does not trigger. hoverBootMove: 1 - # Cost for simple operations on the data card, such as hashing. - dataCardSimple: 0.25 + # Cost for trivial operations on the data card, such as CRC32 or Base64 + dataCardTrivial: 0.2 - # Cost for complex operations on the data card, such as inflate/deflate. - dataCardComplex: 4.0 + # Per-byte cost for trivial operations + dataCardTrivialByte: 0.005 + + # Cost for simple operations on the data card, such as MD5 or AES + dataCardSimple: 1.0 + + # Per-byte cost for simple operations + dataCardSimpleByte: 0.01 + + # Cost for complex operations on the data card, such as SHA256, inflate/deflate and SecureRandom. + dataCardComplex: 6.0 + + # Per-byte cost for complex operations + dataCardComplexByte: 0.1 + + # Cost for asymmetric operations on the data card, such as ECDH and ECDSA + # Per-byte cost for ECDSA operation is controlled by `complex` value, + # because data is hashed with SHA256 before signing/verifying + dataCardAsymmetric: 10.0 } # The rate at which different blocks accept external power. All of these diff --git a/src/main/resources/assets/opencomputers/doc/ru_RU/item/dataCard.md b/src/main/resources/assets/opencomputers/doc/ru_RU/item/dataCard.md index fdcacab78..c18524087 100644 --- a/src/main/resources/assets/opencomputers/doc/ru_RU/item/dataCard.md +++ b/src/main/resources/assets/opencomputers/doc/ru_RU/item/dataCard.md @@ -1,7 +1,12 @@ -# - -![ , .](oredict:oc:dataCard) - - , , . , /. , , , , . - - , , , , . \ No newline at end of file +# Карта данных + +![Вопреки распространенному мнению, не хранит данные.](oredict:oc:dataCard) + +Карта данных предоставляет несколько алгоритмов, которые трудно реализуемы в ОК или работают гораздо медленнее, чем должны. Дополнительно, карта имеет свою файловую систему, в которой есть несколько программ, реализующих данные алгоритмы, также как у интернет карты. + +Не забывайте, что существуют ограничения на размер данных, которые могут быть одновременно обработаны, а также, что выполнение каждой операции тратит энергию. + +Карта имеет три уровня: +* **Первый уровень**: поддерживает кодирование и декодирование Base64, CRC32, MD5, SHA256, сжатие и распаковку данных алгоритмом Deflate +* **Второй уровень**: MD5 и SHA256 могут работать в режиме HMAC, генерация случайных данных, шифрование и расшифровка AES +* **Третий уровень**: ECDSA и ECDH - генерация ключей, подпись данных, выработка сессионных ключей \ No newline at end of file diff --git a/src/main/resources/assets/opencomputers/lang/de_DE.lang b/src/main/resources/assets/opencomputers/lang/de_DE.lang index b8f83a7f7..5889ff81b 100644 --- a/src/main/resources/assets/opencomputers/lang/de_DE.lang +++ b/src/main/resources/assets/opencomputers/lang/de_DE.lang @@ -59,7 +59,9 @@ item.oc.CPU0.name=Hauptprozessor (CPU) (Stufe 1) item.oc.CPU1.name=Hauptprozessor (CPU) (Stufe 2) item.oc.CPU2.name=Hauptprozessor (CPU) (Stufe 3) item.oc.CuttingWire.name=Schneidedraht -item.oc.DataCard.name=Datenkarte +item.oc.DataCard0.name=Datenkarte (Stufe 1) +item.oc.DataCard1.name=Datenkarte (Stufe 2) +item.oc.DataCard2.name=Datenkarte (Stufe 3) item.oc.DebugCard.name=Debug-Karte item.oc.Debugger.name=Netzwerk-Debugger item.oc.Disk.name=Platte @@ -259,7 +261,9 @@ oc:tooltip.ComponentBus=Diese Erweiterung erlaubt es es Servern, mit noch mehr K oc:tooltip.CPU=Kernstück eines jeden Computers. Die Taktrate hat einen leichten Schatten, aber was kann man von einer Taschensonnenuhr schon erwarten?[nl] Unterstützte Komponenten: §f%s§7 oc:tooltip.CPU.Architecture=Architektur: §f%s§7 oc:tooltip.CuttingWire=Wird gebraucht, um Tonblöcke in Leiterplattenform zu bekommen. Vermutlich das ineffizienteste Werkzeug in der Geschichte der Menschheit, da es nach einer Verwendung kaputt geht. -oc:tooltip.DataCard=Stellt einige komplexe Algorithmen wie Hash-Funktionen und deflate/inflate bereit. +oc:tooltip.DataCard0=Stellt einige komplexe Algorithmen wie Hash-Funktionen und deflate/inflate bereit. +oc:tooltip.DataCard1=Stellt einige komplexe Algorithmen wie Hash-Funktionen und deflate/inflate bereit. +oc:tooltip.DataCard2=Stellt einige komplexe Algorithmen wie Hash-Funktionen und deflate/inflate bereit. oc:tooltip.DebugCard=Kreativ-Modus-Gegenstand, erlaubt es die Welt zu manipulieren um das Testen zu erleichtern. Verwendung auf eigene Gefahr. oc:tooltip.Debugger=Erlaubt, Informationen über OCs internes Netzwerk auszugeben. Nur verwenden, wenn von einem Entwickler dazu aufgefordert. oc:tooltip.Disassembler=Zerlegt Gegenstände in ihre Einzelteile. §lWarnung§7: zurückgewonnene Gegenstände haben eine %s%%-ige Chance beim Extrahieren kaputt zu gehen! diff --git a/src/main/resources/assets/opencomputers/lang/en_US.lang b/src/main/resources/assets/opencomputers/lang/en_US.lang index 51d960d4a..7f5fc50f1 100644 --- a/src/main/resources/assets/opencomputers/lang/en_US.lang +++ b/src/main/resources/assets/opencomputers/lang/en_US.lang @@ -60,7 +60,9 @@ item.oc.CPU0.name=Central Processing Unit (CPU) (Tier 1) item.oc.CPU1.name=Central Processing Unit (CPU) (Tier 2) item.oc.CPU2.name=Central Processing Unit (CPU) (Tier 3) item.oc.CuttingWire.name=Cutting Wire -item.oc.DataCard.name=Data Card +item.oc.DataCard0.name=Data Card (Tier 1) +item.oc.DataCard1.name=Data Card (Tier 2) +item.oc.DataCard2.name=Data Card (Tier 3) item.oc.DebugCard.name=Debug Card item.oc.Debugger.name=Network Debugger item.oc.Disk.name=Disk Platter @@ -261,7 +263,9 @@ oc:tooltip.ComponentBus=This expansion allows servers to communicate with more c oc:tooltip.CPU=An essential component of all computers. The clock rate is a bit unreliable, but what do you expect when it runs on a pocket sundial?[nl] Supported components: §f%s§7 oc:tooltip.CPU.Architecture=Architecture: §f%s§7 oc:tooltip.CuttingWire=Used to cut clay blocks into circuit board shape. Breaks after one use, which probably makes it the most inefficient tool ever. -oc:tooltip.DataCard=Provides a couple of advanced algorithms such as hashing and deflate/inflate. +oc:tooltip.DataCard0=Provides a couple of advanced algorithms such as hashing and deflate/inflate. +oc:tooltip.DataCard1=Provides a couple of advanced algorithms such as hashing, AES encryption and deflate/inflate. +oc:tooltip.DataCard2=Provides a couple of advanced algorithms such as hashing, AES encryption, elliptic curve cryptography and deflate/inflate. oc:tooltip.DebugCard=Creative mode item, allows manipulating the world to make testing easier. Use at your own peril. oc:tooltip.Debugger=Can be used to output debug information on OC's internal network grid. Only use if so instructed by a dev. oc:tooltip.Disassembler=Separates items into their original components. §lWarning§7: returned items have a %s%% chance of breaking in the process! diff --git a/src/main/resources/assets/opencomputers/lang/ru_RU.lang b/src/main/resources/assets/opencomputers/lang/ru_RU.lang index 658abedaf..a1ab2d83f 100644 --- a/src/main/resources/assets/opencomputers/lang/ru_RU.lang +++ b/src/main/resources/assets/opencomputers/lang/ru_RU.lang @@ -260,7 +260,9 @@ oc:tooltip.ComponentBus=Позволяет серверам взаимодейс oc:tooltip.CPU=Является важным компонентом в компьютере. Тактовая частота немного нестабильна, но что вы ожидали, ведь он работает даже на карманных солнечных часах. Количество поддерживаемых компонентов: §f%s§7 oc:tooltip.CPU.Architecture=Архитектура: §f%s§7 oc:tooltip.CuttingWire=Используется для нарезки глиняных блоков в пластины. Рвётся после использования, что делает её очень неэффективным инструментом. -oc:tooltip.DataCard=Обеспечивает поддержку нескольких алгоритмов хеширования, а также сжатия. +oc:tooltip.DataCard0=Обеспечивает поддержку нескольких алгоритмов хеширования, а также сжатия. +oc:tooltip.DataCard1=Обеспечивает поддержку шифрования, нескольких алгоритмов хеширования, а также сжатия. +oc:tooltip.DataCard2=Обеспечивает поддержку шифрования и эллиптической криптографии, нескольких алгоритмов хеширования, а также сжатия. oc:tooltip.DebugCard=Креативный предмет, позволяет манипулировать игровым миром. Используйте на свой страх и риск. oc:tooltip.Debugger=Может быть использован для вывода отладочной информации о внутренней сети. oc:tooltip.Disassembler=Разделяет предметы на исходные компоненты. §lВнимание§7: возвращённые предметы имеют шанс %s%% сломаться! diff --git a/src/main/resources/assets/opencomputers/lua/component/data/bin/gpg.lua b/src/main/resources/assets/opencomputers/lua/component/data/bin/gpg.lua new file mode 100644 index 000000000..d7c92f972 --- /dev/null +++ b/src/main/resources/assets/opencomputers/lua/component/data/bin/gpg.lua @@ -0,0 +1,241 @@ +--[[ +-- A program that allows user to perform all crypto operations provided by Tier II / Tier III data cards +-- Author: makkarpov +--]] + +local shell = require("shell") +local data = require("data") +local term = require("term") +local filesystem = require("filesystem") +local serialization = require("serialization") + +local args, options = shell.parse(...) + +local function writeFile(path, data) + if filesystem.exists(path) then + print("gpg: failed to write file: " .. path) + print("gpg: error was: file already exists") + return false + end + + if type(data) == "table" then + data = serialization.serialize(data) + end + + local h, err = io.open(path, "wb") + + if not h then + print("gpg: failed to write file: " .. path) + print("gpg: error was: " .. err) + return false + end + + h:write(data) + h:close() + return true +end + +local function readFile(path, deserialize) + local h = io.open(path, "rb") + local r = h:read("*a") + h:close() + + if deserialize then + r = serialization.unserialize(r) + end + + return r +end + +local function parseKey(path, isPublic) + local d = readFile(path, true) + local k, err = data.deserializeKey(d.d, d.t) + + if not k then + print("gpg: failed to parse key: " .. err) + return nil + end + + if k.isPublic() ~= isPublic then + print("gpg: wrong key type") + return nil + end + + return k +end + +local function deriveName(base, encrypt) + if encrypt then + return base .. ".gpg" + else + local d = base:gsub(".gpg", "") + if d == base then + d = d .. ".dec" + print("gpg: decrypting to " .. d) + end + return d + end +end + +local function ensureTier(tier) + if not require("component").isAvailable("data") then + print("gpg: you must have data card in order to run this program") + error("data card is absent") + end + + if data.tier() < tier then + print("gpg: you must have tier " .. tier .. " data card in order to run this program") + error("data card is too simple") + end +end + +if options['g'] and (#args == 2) then + ensureTier(3) + local pub, priv = data.generateKeyPair(384) + + priv = { t = priv.keyType(), d = priv.serialize() } + pub = { t = pub.keyType(), d = pub.serialize() } + + if not writeFile(args[1], priv) then + print("gpg: failed to write private key, aborting") + return false + end + + if not writeFile(args[2], pub) then + print("gpg: failed to write public key, aborting") + return false + end + + return true +end + +if options['c'] and (options['e'] or options['d']) and (#args == 1) then + ensureTier(2) + if options['d'] and options['e'] then + print("gpg: please specify either -d or -e") + return false + end + + io.write("gpg: enter password: ") + local aesKey = data.md5(term.read(nil, nil, nil, "*")) + local checkValue = data.sha256(aesKey) + + if options['e'] then + local iv = data.random(16) + local d = data.encrypt(readFile(args[1]), aesKey, iv) + + return writeFile(deriveName(args[1], true), { + t = "pwd", + kdf = "md5", + iv = iv, + cv = checkValue, + d = d + }) + else + local d = readFile(args[1], true) + + if d.t ~= "pwd" then + print("gpg: file is not encrypted with a password") + return false + end + + if checkValue ~= d.cv then + print("gpg: password incorrect") + return false + end + + return writeFile(deriveName(args[1], false), data.decrypt(d.d, aesKey, d.iv)) + end +end + +if (options['d'] or options['e']) and (#args == 2) then + ensureTier(3) + if options['d'] and options['e'] then + print("gpg: please specify either -d or -e") + return false + end + + if options['e'] then + local userPub = parseKey(args[1], true) + local tmpPub, tmpPriv = data.generateKeyPair(384) + local aesKey = data.md5(data.ecdh(tmpPriv, userPub)) + local checkValue = data.sha256(aesKey) + local iv = data.random(16) + + local d = data.encrypt(readFile(args[2]), aesKey, iv) + return writeFile(deriveName(args[2], true), { + t = "ecdh", + kdf = "md5", + iv = iv, + cv = checkValue, + k = { + t = tmpPub.keyType(), + d = tmpPub.serialize() + }, + d = d + }) + else + local userPriv = parseKey(args[1], false) + local d = readFile(args[2], true) + + if d.t ~= "ecdh" then + print("gpg: file is not encrypted with a key") + return false + end + + local tmpPub = data.deserializeKey(d.k.d, d.k.t) + local aesKey = data.md5(data.ecdh(userPriv, tmpPub)) + + if d.cv ~= data.sha256(aesKey) then + print("gpg: invalid key") + return false + end + + return writeFile(deriveName(args[2], false), data.decrypt(d.d, aesKey, d.iv)) + end +end + +if (options['s'] or options['v']) and (#args == 2) then + ensureTier(2) + if options['s'] and options['v'] then + print("gpg: please specify either -s or -v") + return false + end + + if options['s'] then + local userPriv = parseKey(args[1], false) + local sign = data.ecdsa(readFile(args[2]), userPriv) + + return writeFile(args[2] .. ".sig", { + t = "ecdsa", + s = sign + }) + else + local userPub = parseKey(args[1], true) + local sign = readFile(args[2] .. ".sig", true) + + if sign.t ~= "ecdsa" then + print("gpg: unsupported signature type") + return false + end + + if not data.ecdsa(readFile(args[2]), userPub, sign.s) then + print("gpg: signature verification failed") + return false + end + + print("gpg: signature is valid") + + return true + end +end + +io.write("Usages:\n") +io.write("gpg -ce -- encrypt file with password\n") +io.write("gpg -cd -- decrypt file with password\n") +io.write("gpg -e -- encrypt file\n") +io.write("gpg -d -- decrypt file\n") +io.write("gpg -g -- generate keypair\n") +io.write("gpg -s -- sign file\n") +io.write("gpg -v -- verify file\n") +return false \ No newline at end of file diff --git a/src/main/resources/assets/opencomputers/lua/component/data/lib/data.lua b/src/main/resources/assets/opencomputers/lua/component/data/lib/data.lua index f5cf7cb75..62d11cef5 100644 --- a/src/main/resources/assets/opencomputers/lua/component/data/lib/data.lua +++ b/src/main/resources/assets/opencomputers/lua/component/data/lib/data.lua @@ -12,45 +12,27 @@ end -- Converts hexadecimal string into binary data. function data.fromHex(hex) - return (data:gsub('..', function (cc) + return (hex:gsub('..', function (cc) return string.char(tonumber(cc, 16)) end)) end --- Applies base64 encoding. -function data.encode64(data) - return component.data.encode64(data) -end +if component.isAvailable("data") then + local wrappedFunctions = { 'encode64', 'decode64', 'sha256', 'md5', 'crc32', 'deflate', 'inflate', + 'getLimit', 'tier', 'encrypt', 'decrypt', 'random', 'generateKeyPair', + 'deserializeKey', 'ecdh', 'ecdsa' } --- Applies base64 decoding. -function data.decode64(data) - return component.data.decode64(data) -end + function data.present() + return true + end --- Returns raw/binary SHA2-256 hash of data. Common form of presenting SHA is hexadecimal string, see data.toHex. -function data.sha256(data) - return component.data.sha256(data) -end - --- Returns raw/binary MD5 hash of data. Common form of presenting SHA is hexadecimal string, see data.toHex. -function data.md5(data) - return component.data.md5(data) -end - --- Returns raw/binary CRC-32 hash of data. Common form of presenting SHA is hexadecimal string, see data.toHex. -function data.crc32(data) - return component.data.crc32(data) -end - - --- Applies DEFLATE compression. -function data.deflate(data) - return component.data.deflate(data) -end - --- Applies INFLATE decompression. -function data.inflate(data) - return component.data.inflate(data) + for i, v in ipairs(wrappedFunctions) do + data[v] = component.data[v] + end +else + function data.present() + return false + end end return data diff --git a/src/main/resources/assets/opencomputers/recipes/default.recipes b/src/main/resources/assets/opencomputers/recipes/default.recipes index be4e6880f..cbfdf508d 100644 --- a/src/main/resources/assets/opencomputers/recipes/default.recipes +++ b/src/main/resources/assets/opencomputers/recipes/default.recipes @@ -149,6 +149,14 @@ dataCard { input: [[nuggetIron, "oc:materialALU", "oc:circuitChip2"] ["", "oc:materialCard", ""]] } +dataCard2 { + input: [[nuggetGold, "oc:materialALU", "oc:circuitChip3"] + ["", "oc:dataCard", ""]] +} +dataCard3 { + input: [[diamond, "oc:cpu2", "oc:circuitChip3"] + ["", "oc:dataCard2", ""]] +} graphicsCard1 { input: [["oc:circuitChip1", "oc:materialALU", "oc:ram1"] ["", "oc:materialCard", ""]] diff --git a/src/main/resources/assets/opencomputers/textures/items/DataCard.png b/src/main/resources/assets/opencomputers/textures/items/DataCard0.png similarity index 100% rename from src/main/resources/assets/opencomputers/textures/items/DataCard.png rename to src/main/resources/assets/opencomputers/textures/items/DataCard0.png diff --git a/src/main/resources/assets/opencomputers/textures/items/DataCard.png.mcmeta b/src/main/resources/assets/opencomputers/textures/items/DataCard0.png.mcmeta similarity index 100% rename from src/main/resources/assets/opencomputers/textures/items/DataCard.png.mcmeta rename to src/main/resources/assets/opencomputers/textures/items/DataCard0.png.mcmeta diff --git a/src/main/resources/assets/opencomputers/textures/items/DataCard1.png b/src/main/resources/assets/opencomputers/textures/items/DataCard1.png new file mode 100644 index 0000000000000000000000000000000000000000..e17e97bde5da8cd404f3697b8aed2b3bee1beaad GIT binary patch literal 749 zcmVhp3=#^38XxGoPPikCrfE#Pneb^_ z)w(;oh{a-!`+P)*`8zmwJi?m0&XdP8+`gB_wo802)P7}957%{Z90vf~wh2UzkS`Um zZ9fa&U}(g^vZlE*dID?l>vjRp^Ke}kfMT)8vq!Hu8SCc8&3-&LMY-(YI1bOB&Xc-) zv{}IOJgU_y<#M?pc=ygN04^9A-cDML?5RWkk zZcYXU27GOY+nV-6eHnFKr(7K-@L~QnXVTZ2c;MfO2ae;gw6wG#QYv8EB|d$z@!S+IUQSc<*0B~h zynu)xB4o2!(RE!6!w|zTMAI}Go0^eqHY-x>luRZi@pxQXT3SSkoiccOP({erTy;=z8Y5BAUh1yvs?J1ZWj f^8>|${cfh8d@ya?ff zjnJ(Wx~q>;P!tMz1kw_#O?Lv=`2*kKBF^2Db=gIT~rXJm8%W?QJ|L)g0s8lMru8Zrsg3b$#*5k6ThiAT zVtDwzwzM=YmbJscKo8CLx@rH3m=q6=kOxnnJ!JCgo%VS!92{zo2e`FA@}NZv<#L%~ zvDk>{#`P-zjE-I8{l_(ScXt8E-JGEGyJb{7P;yp0==S{JXC7#2I^)5)k#R~NRuvBv z4-^mn{d%F92S-~k;MO|hLGsdBHhQCq2g-q}7Y?gl_ new component.DataCard.Tier1() + case Tier.Two => new component.DataCard.Tier2() + case Tier.Three => new component.DataCard.Tier3() + } override def slot(stack: ItemStack) = Slot.Card + override def tier(stack: ItemStack) = + Delegator.subItem(stack) match { + case Some(data: common.item.DataCard) => data.tier + case _ => Tier.One + } + override def providedEnvironment(stack: ItemStack) = classOf[component.DataCard] } diff --git a/src/main/scala/li/cil/oc/server/component/DataCard.scala b/src/main/scala/li/cil/oc/server/component/DataCard.scala index b6e12d76a..c108560dc 100644 --- a/src/main/scala/li/cil/oc/server/component/DataCard.scala +++ b/src/main/scala/li/cil/oc/server/component/DataCard.scala @@ -1,19 +1,17 @@ package li.cil.oc.server.component -import java.util.zip.DeflaterOutputStream -import java.util.zip.InflaterOutputStream +import java.security._ +import java.security.interfaces.{ECPrivateKey, ECPublicKey} +import java.security.spec.{PKCS8EncodedKeySpec, X509EncodedKeySpec} +import java.util.zip.{DeflaterOutputStream, InflaterOutputStream} +import javax.crypto.spec.{IvParameterSpec, SecretKeySpec} +import javax.crypto.{Cipher, KeyAgreement, Mac} import com.google.common.hash.Hashing -import li.cil.oc.OpenComputers -import li.cil.oc.Settings -import li.cil.oc.api -import li.cil.oc.api.Network -import li.cil.oc.api.machine.Arguments -import li.cil.oc.api.machine.Callback -import li.cil.oc.api.machine.Context -import li.cil.oc.api.network.Node -import li.cil.oc.api.network.Visibility -import li.cil.oc.api.prefab +import li.cil.oc.{OpenComputers, Settings, api} +import li.cil.oc.api.machine.{Arguments, Callback, Context} +import li.cil.oc.api.network.{Node, Visibility} +import li.cil.oc.api.{Network, prefab} import li.cil.oc.util.ExtendedNBT._ import net.minecraft.nbt.NBTTagCompound import org.apache.commons.codec.binary.Base64 @@ -28,67 +26,30 @@ class DataCard extends prefab.ManagedEnvironment { val romData = Option(api.FileSystem.asManagedEnvironment(api.FileSystem. fromClass(OpenComputers.getClass, Settings.resourceDomain, "lua/component/data"), "data")) - @Callback(direct = true, doc = """function():number -- The maximum size of data that can be passed to other functions of the card.""") - def getLimit(context: Context, args: Arguments): Array[AnyRef] = { - result(Settings.get.dataCardHardLimit) - } - - @Callback(direct = true, limit = 32, doc = """function(data:string):string -- Applies base64 encoding to the data.""") - def encode64(context: Context, args: Arguments): Array[AnyRef] = { - result(Base64.encodeBase64(checkLimits(context, args, Settings.get.dataCardComplex))) - } - - @Callback(direct = true, limit = 32, doc = """function(data:string):string -- Applies base64 decoding to the data.""") - def decode64(context: Context, args: Arguments): Array[AnyRef] = { - result(Base64.decodeBase64(checkLimits(context, args, Settings.get.dataCardComplex))) - } - - @Callback(direct = true, limit = 6, doc = """function(data:string):string -- Applies deflate compression to the data.""") - def deflate(context: Context, args: Arguments): Array[AnyRef] = { - val data = checkLimits(context, args, Settings.get.dataCardComplex) - val baos = new ByteArrayOutputStream(512) - val deos = new DeflaterOutputStream(baos) - deos.write(data) - deos.finish() - result(baos.toByteArray) - } - - @Callback(direct = true, limit = 6, doc = """function(data:string):string -- Applies inflate decompression to the data.""") - def inflate(context: Context, args: Arguments): Array[AnyRef] = { - val data = checkLimits(context, args, Settings.get.dataCardComplex) - val baos = new ByteArrayOutputStream(512) - val inos = new InflaterOutputStream(baos) - inos.write(data) - inos.finish() - result(baos.toByteArray) - } - - @Callback(direct = true, limit = 32, doc = """function(data:string):string -- Computes SHA2-256 hash of the data. Result is in binary format.""") - def sha256(context: Context, args: Arguments): Array[AnyRef] = { - val data = checkLimits(context, args, Settings.get.dataCardSimple) - result(Hashing.sha256().hashBytes(data).asBytes()) - } - - @Callback(direct = true, limit = 32, doc = """function(data:string):string -- Computes MD5 hash of the data. Result is in binary format""") - def md5(context: Context, args: Arguments): Array[AnyRef] = { - val data = checkLimits(context, args, Settings.get.dataCardSimple) - result(Hashing.md5().hashBytes(data).asBytes()) - } - - @Callback(direct = true, limit = 32, doc = """function(data:string):string -- Computes CRC-32 hash of the data. Result is in binary format""") - def crc32(context: Context, args: Arguments): Array[AnyRef] = { - val data = checkLimits(context, args, Settings.get.dataCardSimple) - result(Hashing.crc32().hashBytes(data).asBytes()) - } - - private def checkLimits(context: Context, args: Arguments, cost: Double): Array[Byte] = { + protected def checkCost(context: Context, args: Arguments, baseCost: Double, byteCost: Double): Array[Byte] = { val data = args.checkByteArray(0) if (data.length > Settings.get.dataCardHardLimit) throw new IllegalArgumentException("data size limit exceeded") - if (!node.tryChangeBuffer(-cost)) throw new Exception("not enough energy") + if (!node.tryChangeBuffer(-baseCost - data.length * byteCost)) throw new Exception("not enough energy") if (data.length > Settings.get.dataCardSoftLimit) context.pause(Settings.get.dataCardTimeout) data } + protected def checkCost(baseCost: Double): Unit = { + if (!node.tryChangeBuffer(-baseCost)) throw new Exception("not enough energy") + } + + protected def trivialCost(context: Context, args: Arguments) = + checkCost(context, args, Settings.get.dataCardTrivial, Settings.get.dataCardTrivialByte) + + protected def simpleCost(context: Context, args: Arguments) = + checkCost(context, args, Settings.get.dataCardSimple, Settings.get.dataCardSimpleByte) + + protected def complexCost(context: Context, args: Arguments) = + checkCost(context, args, Settings.get.dataCardComplex, Settings.get.dataCardComplexByte) + + protected def asymmetricCost(context: Context, args: Arguments) = + checkCost(context, args, Settings.get.dataCardAsymmetric, Settings.get.dataCardComplexByte) + // ----------------------------------------------------------------------- // override def onConnect(node: Node) { @@ -115,3 +76,251 @@ class DataCard extends prefab.ManagedEnvironment { romData.foreach(fs => nbt.setNewCompoundTag("romData", fs.save)) } } + +object DataCard { + val SecureRandomInstance = new ThreadLocal[SecureRandom]() { + override def initialValue = SecureRandom.getInstance("SHA1PRNG") + } + + class Tier1 extends DataCard { + @Callback(direct = true, doc = """function():number -- The maximum size of data that can be passed to other functions of the card.""") + def getLimit(context: Context, args: Arguments): Array[AnyRef] = { + result(Settings.get.dataCardHardLimit) + } + + @Callback(direct = true, limit = 32, doc = """function(data:string):string -- Applies base64 encoding to the data.""") + def encode64(context: Context, args: Arguments): Array[AnyRef] = { + result(Base64.encodeBase64(trivialCost(context, args))) + } + + @Callback(direct = true, limit = 32, doc = """function(data:string):string -- Applies base64 decoding to the data.""") + def decode64(context: Context, args: Arguments): Array[AnyRef] = { + result(Base64.decodeBase64(trivialCost(context, args))) + } + + @Callback(direct = true, limit = 4, doc = """function(data:string):string -- Applies deflate compression to the data.""") + def deflate(context: Context, args: Arguments): Array[AnyRef] = { + val data = complexCost(context, args) + val baos = new ByteArrayOutputStream(512) + val deos = new DeflaterOutputStream(baos) + deos.write(data) + deos.finish() + result(baos.toByteArray) + } + + @Callback(direct = true, limit = 4, doc = """function(data:string):string -- Applies inflate decompression to the data.""") + def inflate(context: Context, args: Arguments): Array[AnyRef] = { + val data = complexCost(context, args) + val baos = new ByteArrayOutputStream(512) + val inos = new InflaterOutputStream(baos) + inos.write(data) + inos.finish() + result(baos.toByteArray) + } + + @Callback(direct = true, limit = 4, doc = """function(data:string):string -- Computes SHA2-256 hash of the data. Result is in binary format.""") + def sha256(context: Context, args: Arguments): Array[AnyRef] = { + val data = complexCost(context, args) + result(Hashing.sha256().hashBytes(data).asBytes()) + } + + @Callback(direct = true, limit = 8, doc = """function(data:string):string -- Computes MD5 hash of the data. Result is in binary format""") + def md5(context: Context, args: Arguments): Array[AnyRef] = { + val data = simpleCost(context, args) + result(Hashing.md5().hashBytes(data).asBytes()) + } + + @Callback(direct = true, limit = 32, doc = """function(data:string):string -- Computes CRC-32 hash of the data. Result is in binary format""") + def crc32(context: Context, args: Arguments): Array[AnyRef] = { + val data = trivialCost(context, args) + result(Hashing.crc32().hashBytes(data).asBytes()) + } + + @Callback(direct = true, limit = 32, doc = """function():number -- Returns a tier of the card""") + def tier(context: Context, args: Arguments): Array[AnyRef] = result(1) + } + + class Tier2 extends Tier1 { + @Callback(direct = true, limit = 32, doc = """function():number -- Returns a tier of the card""") + override def tier(context: Context, args: Arguments): Array[AnyRef] = result(2) + + private def crypt(context: Context, args: Arguments, mode: Int): Array[AnyRef] = { + val data = simpleCost(context, args) + + val key = args.checkByteArray(1) + if (key.length != 16) + throw new IllegalArgumentException("Expected a 128-bit AES key") + + val iv = args.checkByteArray(2) + if (iv.length != 16) + throw new IllegalArgumentException("Expected a 128-bit AES IV") + + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + cipher.init(mode, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv)) + result(cipher.doFinal(data)) + } + + @Callback(direct = true, limit = 8, doc = """function(data:string, key: string, iv:string):string -- Encrypt data with AES. Result is in binary format.""") + def encrypt(context: Context, args: Arguments): Array[AnyRef] = crypt(context, args, Cipher.ENCRYPT_MODE) + + @Callback(direct = true, limit = 8, doc = """function(data:string, key:string, iv:string):string -- Decrypt data with AES""") + def decrypt(context: Context, args: Arguments): Array[AnyRef] = crypt(context, args, Cipher.DECRYPT_MODE) + + private def hash(context: Context, args: Arguments, mode: String, hmacMode: String, simple: Boolean = false): Array[AnyRef] = { + val data = if (simple) simpleCost(context, args) else complexCost(context, args) + + if (args.count() > 1) { + val key = args.checkByteArray(1) + + val hmac = Mac.getInstance(hmacMode) + hmac.init(new SecretKeySpec(key, hmacMode)) + result(hmac.doFinal(data)) + } else { + result(MessageDigest.getInstance(mode).digest(data)) + } + } + + @Callback(direct = true, limit = 4, doc = """function(data:string[, hmacKey:string]):string -- Computes SHA2-256 hash of the data. Result is in binary format.""") + override def sha256(context: Context, args: Arguments): Array[AnyRef] = hash(context, args, "SHA-256", "HmacSHA256") + + @Callback(direct = true, limit = 8, doc = """function(data:string[, hmacKey:string]):string -- Computes MD5 hash of the data. Result is in binary format""") + override def md5(context: Context, args: Arguments): Array[AnyRef] = hash(context, args, "MD5", "HmacMD5", simple = true) + + @Callback(direct = true, limit = 4, doc = """function(len:number):string -- Generates secure random binary data""") + def random(context: Context, args: Arguments): Array[AnyRef] = { + checkCost(Settings.get.dataCardComplex) + val len = args.checkInteger(0) + + if (len <= 0 || len > 1024) + throw new IllegalArgumentException("Length must be in range [1..1024]") + + val target = new Array[Byte](len) + SecureRandomInstance.get.nextBytes(target) + result(target) + } + } + + object ECUserdata { + def deserializeKey(t: String, data: Array[Byte]): Key = { + val fact = KeyFactory.getInstance("EC") + + t match { + case "ec-private" => fact.generatePrivate(new PKCS8EncodedKeySpec(data)) + case "ec-public" => fact.generatePublic(new X509EncodedKeySpec(data)) + case _ => throw new IllegalArgumentException("Wrong key type. Currently supported: ec-public, ec-private") + } + } + } + + class ECUserdata extends prefab.AbstractValue { + var k: Key = null + + // Hack to keep empty constructor for deserialization + def this(_k: Key) = { + this() + k = _k + } + + private def keyType = k match { + case x: ECPrivateKey => "ec-private" + case x: ECPublicKey => "ec-public" + } + + @Callback(direct = true, limit = 32, doc = "function():string -- Returns type of key") + def keyType(context: Context, args: Arguments): Array[AnyRef] = result(keyType) + + @Callback(direct = true, limit = 4, doc = "function():string -- Returns string representation of key. Result is in binary format.") + def serialize(context: Context, args: Arguments): Array[AnyRef] = result(k.getEncoded) + + @Callback(direct = true, limit = 32, doc = "function():boolean -- Returns whether key is public") + def isPublic(context: Context, args: Arguments): Array[AnyRef] = result(isPublic) + + def isPublic = k.isInstanceOf[ECPublicKey] + + override def load(nbt: NBTTagCompound): Unit = + k = ECUserdata.deserializeKey(nbt.getString("Type"), nbt.getByteArray("Data")) + + override def save(nbt: NBTTagCompound): Unit = { + nbt.setString("Type", keyType) + nbt.setByteArray("Data", k.getEncoded) + } + } + + class Tier3 extends Tier2 { + @Callback(direct = true, limit = 32, doc = """function():number -- Returns a tier of the card""") + override def tier(context: Context, args: Arguments): Array[AnyRef] = result(3) + + @Callback(direct = true, limit = 1, doc = """function([bitLen:number]):eckey,eckey -- Generates key pair. Returns: public, private keys. Allowed key lengths: 256, 384 bits""") + def generateKeyPair(context: Context, args: Arguments): Array[AnyRef] = { + checkCost(Settings.get.dataCardAsymmetric) + var bitLen = 384 + + if (args.count() > 0) { + bitLen = args.checkInteger(0) + + if (bitLen != 256 && bitLen != 384) + throw new IllegalArgumentException("Invalid key length. Allowed: 256, 384") + } + + val kpg = KeyPairGenerator.getInstance("EC") + kpg.initialize(bitLen, SecureRandomInstance.get) + val kp = kpg.generateKeyPair() + + result(new ECUserdata(kp.getPublic), new ECUserdata(kp.getPrivate)) + } + + @Callback(direct = true, limit = 8, doc = """function(data:string, type:string):eckey -- Restores key from its string representation.""") + def deserializeKey(context: Context, args: Arguments): Array[AnyRef] = { + val data = simpleCost(context, args) + val t = args.checkString(1) + + result(new ECUserdata(ECUserdata.deserializeKey(t, data))) + } + + private def checkUserdata(args: Arguments, i: Int, isPublic: Boolean = false, anyAccepted: Boolean = false) = + args.checkAny(i) match { + case x: ECUserdata => + if (anyAccepted || x.isPublic == isPublic) x + else throw new IllegalArgumentException((if (isPublic) "Public" else "Private") + " key expected at " + i) + case x => throw new IllegalArgumentException("Userdata expected at " + i) + } + + @Callback(direct = true, limit = 1, doc = """function(priv:eckey, pub:eckey):string -- Generates a shared key. ecdh(a.priv, b.pub) == ecdh(b.priv, a.pub)""") + def ecdh(context: Context, args: Arguments): Array[AnyRef] = { + checkCost(Settings.get.dataCardAsymmetric) + val privKey = checkUserdata(args, 0, isPublic = false).k + val pubKey = checkUserdata(args, 1, isPublic = true).k + + val ka = KeyAgreement.getInstance("ECDH") + ka.init(privKey) + ka.doPhase(pubKey, true) + result(ka.generateSecret) + } + + @Callback(direct = true, limit = 1, doc = """function(data:string, key:eckey[, sig:string]):string|boolean -- Signs or verifies data""") + def ecdsa(context: Context, args: Arguments): Array[AnyRef] = { + val data = asymmetricCost(context, args) + val key = checkUserdata(args, 1, anyAccepted = true) + val sig = args.optByteArray(2, null) + + val sign = Signature.getInstance("SHA256withECDSA") + if (sig != null) { + // Verify mode + if (!key.isPublic) + throw new IllegalArgumentException("Public key expected") + + sign.initVerify(key.k.asInstanceOf[PublicKey]) + sign.update(data) + result(sign.verify(sig)) + } else { + // Sign mode + if (key.isPublic) + throw new IllegalArgumentException("Private key expected") + + sign.initSign(key.k.asInstanceOf[PrivateKey]) + sign.update(data) + result(sign.sign()) + } + } + } +} \ No newline at end of file