Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Implement generateKey for HMAC and AES-CBC
https://bugs.webkit.org/show_bug.cgi?id=123669

Reviewed by Dan Bernstein.

Source/WebCore:

Tests: crypto/subtle/aes-cbc-generate-key.html
       crypto/subtle/hmac-generate-key.html

* WebCore.xcodeproj/project.pbxproj: Added new files.

* bindings/js/JSCryptoAlgorithmDictionary.cpp:
(WebCore::createAesKeyGenParams): Added bindings for AesKeyGenParams.
(WebCore::JSCryptoAlgorithmDictionary::createParametersForGenerateKey): Handle
algorithms that generate AES and HMAC keys.

* bindings/js/JSSubtleCryptoCustom.cpp: (WebCore::JSSubtleCrypto::generateKey): Added.

* crypto/CryptoAlgorithmAesKeyGenParams.h: Added.

* crypto/CryptoKey.cpp: (WebCore::CryptoKey::randomData):
* crypto/CryptoKey.h:
* crypto/CryptoKeyMac.cpp: Added
Expose a function that produces random data for symmetric crypto keys. Cross-platform
implementation uses ARC4 code from WTF, while Mac uses a system function that
provides a FIPS validated random number generator.

* crypto/CryptoKeyAES.cpp: (WebCore::CryptoKeyAES::generate):
* crypto/CryptoKeyAES.h:
Added a function that creates AES keys.

* crypto/SubtleCrypto.idl: Added generateKey.

* crypto/algorithms/CryptoAlgorithmAES_CBC.cpp:
(WebCore::CryptoAlgorithmAES_CBC::generateKey): Added.

* crypto/algorithms/CryptoAlgorithmHMAC.cpp:
(WebCore::CryptoAlgorithmHMAC::generateKey): Added.

* crypto/keys/CryptoKeyHMAC.cpp: (WebCore::CryptoKeyHMAC::generate):
* crypto/keys/CryptoKeyHMAC.h:
Added a function that creates HMAC keys.

* crypto/mac/CryptoAlgorithmAES_CBCMac.cpp: Removed generateKey stub, the implementation
ended up in cross-platform file.

* crypto/mac/CryptoAlgorithmHMACMac.cpp: Ditto.

LayoutTests:

* crypto/subtle/aes-cbc-generate-key-expected.txt: Added.
* crypto/subtle/aes-cbc-generate-key.html: Added.
* crypto/subtle/hmac-generate-key-expected.txt: Added.
* crypto/subtle/hmac-generate-key.html: Added.

* crypto/subtle/sha-1-expected.txt: Now that crypto.webkitSubtle.generateKey exists,
a different exception is raised.


Canonical link: https://commits.webkit.org/141879@main
git-svn-id: https://svn.webkit.org/repository/webkit/trunk@158526 268f45cc-cd09-0410-ab3c-d52691b4dbfc
  • Loading branch information
aproskuryakov committed Nov 3, 2013
1 parent 69644d2 commit f656051
Show file tree
Hide file tree
Showing 23 changed files with 454 additions and 19 deletions.
15 changes: 15 additions & 0 deletions LayoutTests/ChangeLog
@@ -1,3 +1,18 @@
2013-11-02 Alexey Proskuryakov <ap@apple.com>

Implement generateKey for HMAC and AES-CBC
https://bugs.webkit.org/show_bug.cgi?id=123669

Reviewed by Dan Bernstein.

* crypto/subtle/aes-cbc-generate-key-expected.txt: Added.
* crypto/subtle/aes-cbc-generate-key.html: Added.
* crypto/subtle/hmac-generate-key-expected.txt: Added.
* crypto/subtle/hmac-generate-key.html: Added.

* crypto/subtle/sha-1-expected.txt: Now that crypto.webkitSubtle.generateKey exists,
a different exception is raised.

2013-11-02 Andreas Kling <akling@apple.com>

Optimize baselines: css3
Expand Down
19 changes: 19 additions & 0 deletions LayoutTests/crypto/subtle/aes-cbc-generate-key-expected.txt
@@ -0,0 +1,19 @@
Test generating an AES key using AES-CBC algorithm.

On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".


PASS crypto.subtle.generateKey("aes-cbc", extractable, ["encrypt", "decrypt"]) threw exception TypeError: Type error.
PASS crypto.subtle.generateKey({name: "aes-cbc"}, extractable, ["encrypt", "decrypt"]) threw exception TypeError: Type error.
PASS crypto.subtle.generateKey({name: "aes-cbc", length: undefined}, extractable, ["encrypt", "decrypt"]) threw exception TypeError: Type error.
PASS crypto.subtle.generateKey({name: "aes-cbc", length: {}}, extractable, ["encrypt", "decrypt"]) threw exception TypeError: Type error.
Generating a key...
PASS key.type is 'secret'
PASS key.extractable is true
PASS key.algorithm.name is 'aes-cbc'
PASS key.algorithm.length is 128
PASS key.usages is ['encrypt', 'decrypt']
PASS successfullyParsed is true

TEST COMPLETE

42 changes: 42 additions & 0 deletions LayoutTests/crypto/subtle/aes-cbc-generate-key.html
@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<script src="../../resources/js-test-pre.js"></script>
<script src="resources/common.js"></script>
</head>
<body>
<p id="description"></p>
<div id="console"></div>

<script>
description("Test generating an AES key using AES-CBC algorithm.");

jsTestIsAsync = true;

if (!window.subtle)
window.crypto.subtle = window.crypto.webkitSubtle;

var extractable = true;

shouldThrow('crypto.subtle.generateKey("aes-cbc", extractable, ["encrypt", "decrypt"])');
shouldThrow('crypto.subtle.generateKey({name: "aes-cbc"}, extractable, ["encrypt", "decrypt"])');
shouldThrow('crypto.subtle.generateKey({name: "aes-cbc", length: undefined}, extractable, ["encrypt", "decrypt"])');
shouldThrow('crypto.subtle.generateKey({name: "aes-cbc", length: {}}, extractable, ["encrypt", "decrypt"])');

debug("Generating a key...");
crypto.subtle.generateKey({name: "aes-cbc", length: 128}, extractable, ["encrypt", "decrypt"]).then(function(result) {
key = result;

shouldBe("key.type", "'secret'");
shouldBe("key.extractable", "true");
shouldBe("key.algorithm.name", "'aes-cbc'");
shouldBe("key.algorithm.length", "128");
shouldBe("key.usages", "['encrypt', 'decrypt']");

finishJSTest();
});
</script>

<script src="../../resources/js-test-post.js"></script>
</body>
</html>
27 changes: 27 additions & 0 deletions LayoutTests/crypto/subtle/hmac-generate-key-expected.txt
@@ -0,0 +1,27 @@
Test generating a HMAC key.

On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".


PASS crypto.subtle.generateKey("hmac", extractable, ["sign", "verify"]) threw exception TypeError: Type error.
PASS crypto.subtle.generateKey({name: "hmac"}, extractable, ["sign", "verify"]) threw exception Error: NotSupportedError: DOM Exception 9.
PASS crypto.subtle.generateKey({name: "hmac", length: undefined}, extractable, ["sign", "verify"]) threw exception Error: NotSupportedError: DOM Exception 9.
PASS crypto.subtle.generateKey({name: "hmac", length: {}}, extractable, ["sign", "verify"]) threw exception Error: NotSupportedError: DOM Exception 9.

Generating a key with default length...
PASS key.type is 'secret'
PASS key.extractable is true
PASS key.algorithm.name is 'hmac'
PASS key.algorithm.length is 64
PASS key.usages is ["sign", "verify"]

Generating a key with custom length...
PASS key.type is 'secret'
PASS key.extractable is true
PASS key.algorithm.name is 'hmac'
PASS key.algorithm.length is 5
PASS key.usages is ["sign"]
PASS successfullyParsed is true

TEST COMPLETE

52 changes: 52 additions & 0 deletions LayoutTests/crypto/subtle/hmac-generate-key.html
@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html>
<head>
<script src="../../resources/js-test-pre.js"></script>
<script src="resources/common.js"></script>
</head>
<body>
<p id="description"></p>
<div id="console"></div>

<script>
description("Test generating a HMAC key.");

jsTestIsAsync = true;

if (!window.subtle)
window.crypto.subtle = window.crypto.webkitSubtle;

var extractable = true;

shouldThrow('crypto.subtle.generateKey("hmac", extractable, ["sign", "verify"])');
shouldThrow('crypto.subtle.generateKey({name: "hmac"}, extractable, ["sign", "verify"])');
shouldThrow('crypto.subtle.generateKey({name: "hmac", length: undefined}, extractable, ["sign", "verify"])');
shouldThrow('crypto.subtle.generateKey({name: "hmac", length: {}}, extractable, ["sign", "verify"])');

debug("\nGenerating a key with default length...");
crypto.subtle.generateKey({name: "hmac", hash: "sha-1"}, extractable, ["sign", "verify"]).then(function(result) {
key = result;

shouldBe("key.type", "'secret'");
shouldBe("key.extractable", "true");
shouldBe("key.algorithm.name", "'hmac'");
shouldBe("key.algorithm.length", "64");
shouldBe("key.usages", '["sign", "verify"]');

debug("\nGenerating a key with custom length...");
return crypto.subtle.generateKey({name: "hmac", hash: "sha-1", length: 5}, extractable, ["sign"]);
}).then(function(result) {
key = result;

shouldBe("key.type", "'secret'");
shouldBe("key.extractable", "true");
shouldBe("key.algorithm.name", "'hmac'");
shouldBe("key.algorithm.length", "5");
shouldBe("key.usages", '["sign"]');
finishJSTest();
});
</script>

<script src="../../resources/js-test-post.js"></script>
</body>
</html>
2 changes: 1 addition & 1 deletion LayoutTests/crypto/subtle/sha-1-expected.txt
Expand Up @@ -11,7 +11,7 @@ SHA1 of [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
= [2c 7e 7c 38 4f 78 29 69 42 82 b1 e3 a6 21 6d ef 80 82 d0 55]
SHA1 of [new Uint8Array([0, 1, 2, 3, 4]), new Uint8Array(5, 6, 7, 8, 9, 10])]
= [2c 7e 7c 38 4f 78 29 69 42 82 b1 e3 a6 21 6d ef 80 82 d0 55]
PASS crypto.subtle.generateKey('sha-1') threw exception TypeError: undefined is not a function (evaluating 'crypto.subtle.generateKey('sha-1')').
PASS crypto.subtle.generateKey('sha-1') threw exception Error: NotSupportedError: DOM Exception 9.
PASS successfullyParsed is true

TEST COMPLETE
Expand Down
49 changes: 49 additions & 0 deletions Source/WebCore/ChangeLog
@@ -1,3 +1,52 @@
2013-11-02 Alexey Proskuryakov <ap@apple.com>

Implement generateKey for HMAC and AES-CBC
https://bugs.webkit.org/show_bug.cgi?id=123669

Reviewed by Dan Bernstein.

Tests: crypto/subtle/aes-cbc-generate-key.html
crypto/subtle/hmac-generate-key.html

* WebCore.xcodeproj/project.pbxproj: Added new files.

* bindings/js/JSCryptoAlgorithmDictionary.cpp:
(WebCore::createAesKeyGenParams): Added bindings for AesKeyGenParams.
(WebCore::JSCryptoAlgorithmDictionary::createParametersForGenerateKey): Handle
algorithms that generate AES and HMAC keys.

* bindings/js/JSSubtleCryptoCustom.cpp: (WebCore::JSSubtleCrypto::generateKey): Added.

* crypto/CryptoAlgorithmAesKeyGenParams.h: Added.

* crypto/CryptoKey.cpp: (WebCore::CryptoKey::randomData):
* crypto/CryptoKey.h:
* crypto/CryptoKeyMac.cpp: Added
Expose a function that produces random data for symmetric crypto keys. Cross-platform
implementation uses ARC4 code from WTF, while Mac uses a system function that
provides a FIPS validated random number generator.

* crypto/CryptoKeyAES.cpp: (WebCore::CryptoKeyAES::generate):
* crypto/CryptoKeyAES.h:
Added a function that creates AES keys.

* crypto/SubtleCrypto.idl: Added generateKey.

* crypto/algorithms/CryptoAlgorithmAES_CBC.cpp:
(WebCore::CryptoAlgorithmAES_CBC::generateKey): Added.

* crypto/algorithms/CryptoAlgorithmHMAC.cpp:
(WebCore::CryptoAlgorithmHMAC::generateKey): Added.

* crypto/keys/CryptoKeyHMAC.cpp: (WebCore::CryptoKeyHMAC::generate):
* crypto/keys/CryptoKeyHMAC.h:
Added a function that creates HMAC keys.

* crypto/mac/CryptoAlgorithmAES_CBCMac.cpp: Removed generateKey stub, the implementation
ended up in cross-platform file.

* crypto/mac/CryptoAlgorithmHMACMac.cpp: Ditto.

2013-11-02 Christophe Dumez <ch.dumez@samsung.com>

EnforceRange doesn't enforce range of a short
Expand Down
8 changes: 8 additions & 0 deletions Source/WebCore/WebCore.xcodeproj/project.pbxproj
Expand Up @@ -5581,6 +5581,8 @@
E187056316E54A0D00585E97 /* MainThreadTask.h in Headers */ = {isa = PBXBuildFile; fileRef = E187056216E54A0D00585E97 /* MainThreadTask.h */; };
E18772F1126E2629003DD586 /* Language.cpp in Sources */ = {isa = PBXBuildFile; fileRef = E18772F0126E2629003DD586 /* Language.cpp */; };
E19727161820549E00592D51 /* CryptoKeyType.h in Headers */ = {isa = PBXBuildFile; fileRef = E19727151820549E00592D51 /* CryptoKeyType.h */; };
E19AC3F71824E5D100349426 /* CryptoAlgorithmAesKeyGenParams.h in Headers */ = {isa = PBXBuildFile; fileRef = E19AC3F61824E5D100349426 /* CryptoAlgorithmAesKeyGenParams.h */; };
E19AC3F9182566F700349426 /* CryptoKeyMac.cpp in Sources */ = {isa = PBXBuildFile; fileRef = E19AC3F8182566F700349426 /* CryptoKeyMac.cpp */; };
E19AC3E21824DC6900349426 /* CryptoAlgorithmSHA224Mac.cpp in Sources */ = {isa = PBXBuildFile; fileRef = E19AC3DE1824DC6900349426 /* CryptoAlgorithmSHA224Mac.cpp */; };
E19AC3E31824DC6900349426 /* CryptoAlgorithmSHA256Mac.cpp in Sources */ = {isa = PBXBuildFile; fileRef = E19AC3DF1824DC6900349426 /* CryptoAlgorithmSHA256Mac.cpp */; };
E19AC3E41824DC6900349426 /* CryptoAlgorithmSHA384Mac.cpp in Sources */ = {isa = PBXBuildFile; fileRef = E19AC3E01824DC6900349426 /* CryptoAlgorithmSHA384Mac.cpp */; };
Expand Down Expand Up @@ -12610,6 +12612,8 @@
E187056216E54A0D00585E97 /* MainThreadTask.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MainThreadTask.h; sourceTree = "<group>"; };
E18772F0126E2629003DD586 /* Language.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Language.cpp; sourceTree = "<group>"; };
E19727151820549E00592D51 /* CryptoKeyType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CryptoKeyType.h; sourceTree = "<group>"; };
E19AC3F61824E5D100349426 /* CryptoAlgorithmAesKeyGenParams.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CryptoAlgorithmAesKeyGenParams.h; sourceTree = "<group>"; };
E19AC3F8182566F700349426 /* CryptoKeyMac.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = CryptoKeyMac.cpp; sourceTree = "<group>"; };
E19AC3DE1824DC6900349426 /* CryptoAlgorithmSHA224Mac.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CryptoAlgorithmSHA224Mac.cpp; path = mac/CryptoAlgorithmSHA224Mac.cpp; sourceTree = "<group>"; };
E19AC3DF1824DC6900349426 /* CryptoAlgorithmSHA256Mac.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CryptoAlgorithmSHA256Mac.cpp; path = mac/CryptoAlgorithmSHA256Mac.cpp; sourceTree = "<group>"; };
E19AC3E01824DC6900349426 /* CryptoAlgorithmSHA384Mac.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CryptoAlgorithmSHA384Mac.cpp; path = mac/CryptoAlgorithmSHA384Mac.cpp; sourceTree = "<group>"; };
Expand Down Expand Up @@ -20170,6 +20174,7 @@
E19AC3DF1824DC6900349426 /* CryptoAlgorithmSHA256Mac.cpp */,
E19AC3E01824DC6900349426 /* CryptoAlgorithmSHA384Mac.cpp */,
E19AC3E11824DC6900349426 /* CryptoAlgorithmSHA512Mac.cpp */,
E19AC3F8182566F700349426 /* CryptoKeyMac.cpp */,
);
name = mac;
sourceTree = "<group>";
Expand Down Expand Up @@ -20210,6 +20215,7 @@
isa = PBXGroup;
children = (
E125F8391824104800D84CD9 /* CryptoAlgorithmAesCbcParams.h */,
E19AC3F61824E5D100349426 /* CryptoAlgorithmAesKeyGenParams.h */,
E19DA29B18189ADD00088BC8 /* CryptoAlgorithmHmacKeyParams.h */,
E1C6571E1816E50300256CDD /* CryptoAlgorithmHmacParams.h */,
);
Expand Down Expand Up @@ -23183,6 +23189,7 @@
D359D8BF129CA55C0006E5D2 /* JSHTMLDetailsElement.h in Headers */,
76808B50159DADFA002B5233 /* JSHTMLDialogElement.h in Headers */,
1A85B1E70A1B240500D8C87C /* JSHTMLDirectoryElement.h in Headers */,
E19AC3F71824E5D100349426 /* CryptoAlgorithmAesKeyGenParams.h in Headers */,
1A85B2B70A1B2AC700D8C87C /* JSHTMLDivElement.h in Headers */,
1A85B1E90A1B240500D8C87C /* JSHTMLDListElement.h in Headers */,
1A494E350A12358B00FDAFC1 /* JSHTMLDocument.h in Headers */,
Expand Down Expand Up @@ -27001,6 +27008,7 @@
C6F0900E14327B6100685849 /* MutationObserver.cpp in Sources */,
E19AC3E51824DC6900349426 /* CryptoAlgorithmSHA512Mac.cpp in Sources */,
D6E528A3149A926D00EFE1F3 /* MutationObserverInterestGroup.cpp in Sources */,
E19AC3F9182566F700349426 /* CryptoKeyMac.cpp in Sources */,
D6E276AF14637455001D280A /* MutationObserverRegistration.cpp in Sources */,
C6F08FBC1430FE8F00685849 /* MutationRecord.cpp in Sources */,
52B6C9C515E3F4DF00690B05 /* NamedFlowCollection.cpp in Sources */,
Expand Down
31 changes: 27 additions & 4 deletions Source/WebCore/bindings/js/JSCryptoAlgorithmDictionary.cpp
Expand Up @@ -29,6 +29,7 @@
#if ENABLE(SUBTLE_CRYPTO)

#include "CryptoAlgorithmAesCbcParams.h"
#include "CryptoAlgorithmAesKeyGenParams.h"
#include "CryptoAlgorithmHmacKeyParams.h"
#include "CryptoAlgorithmHmacParams.h"
#include "CryptoAlgorithmRegistry.h"
Expand Down Expand Up @@ -126,7 +127,7 @@ static std::unique_ptr<CryptoAlgorithmParameters> createAesCbcParams(JSC::ExecSt
if (exec->hadException())
return nullptr;

std::unique_ptr<CryptoAlgorithmAesCbcParams> result = std::make_unique<CryptoAlgorithmAesCbcParams>();
auto result = std::make_unique<CryptoAlgorithmAesCbcParams>();

CryptoOperationData ivData;
if (!cryptoOperationDataFromJSValue(exec, iv, ivData)) {
Expand All @@ -144,6 +145,24 @@ static std::unique_ptr<CryptoAlgorithmParameters> createAesCbcParams(JSC::ExecSt
return std::move(result);
}

static std::unique_ptr<CryptoAlgorithmParameters> createAesKeyGenParams(JSC::ExecState* exec, JSC::JSValue value)
{
if (!value.isObject()) {
throwTypeError(exec);
return nullptr;
}

auto result = std::make_unique<CryptoAlgorithmAesKeyGenParams>();

JSValue lengthValue = getProperty(exec, value.getObject(), "length");
if (exec->hadException())
return nullptr;

result->length = toUInt16(exec, lengthValue, EnforceRange);

return std::move(result);
}

static std::unique_ptr<CryptoAlgorithmParameters> createHmacParams(JSC::ExecState* exec, JSC::JSValue value)
{
if (!value.isObject()) {
Expand All @@ -152,7 +171,7 @@ static std::unique_ptr<CryptoAlgorithmParameters> createHmacParams(JSC::ExecStat
}

JSDictionary jsDictionary(exec, value.getObject());
std::unique_ptr<CryptoAlgorithmHmacParams> result = std::make_unique<CryptoAlgorithmHmacParams>();
auto result = std::make_unique<CryptoAlgorithmHmacParams>();

if (!getHashAlgorithm(jsDictionary, result->hash)) {
ASSERT(exec->hadException());
Expand All @@ -170,7 +189,7 @@ static std::unique_ptr<CryptoAlgorithmParameters> createHmacKeyParams(JSC::ExecS
}

JSDictionary jsDictionary(exec, value.getObject());
std::unique_ptr<CryptoAlgorithmHmacKeyParams> result = std::make_unique<CryptoAlgorithmHmacKeyParams>();
auto result = std::make_unique<CryptoAlgorithmHmacKeyParams>();

if (!getHashAlgorithm(jsDictionary, result->hash)) {
ASSERT(exec->hadException());
Expand Down Expand Up @@ -344,7 +363,7 @@ std::unique_ptr<CryptoAlgorithmParameters> JSCryptoAlgorithmDictionary::createPa
}
}

std::unique_ptr<CryptoAlgorithmParameters> JSCryptoAlgorithmDictionary::createParametersForGenerateKey(JSC::ExecState* exec, CryptoAlgorithmIdentifier algorithm, JSC::JSValue)
std::unique_ptr<CryptoAlgorithmParameters> JSCryptoAlgorithmDictionary::createParametersForGenerateKey(JSC::ExecState* exec, CryptoAlgorithmIdentifier algorithm, JSC::JSValue value)
{
switch (algorithm) {
case CryptoAlgorithmIdentifier::RSAES_PKCS1_v1_5:
Expand All @@ -353,12 +372,16 @@ std::unique_ptr<CryptoAlgorithmParameters> JSCryptoAlgorithmDictionary::createPa
case CryptoAlgorithmIdentifier::RSA_OAEP:
case CryptoAlgorithmIdentifier::ECDSA:
case CryptoAlgorithmIdentifier::ECDH:
setDOMException(exec, NOT_SUPPORTED_ERR);
return nullptr;
case CryptoAlgorithmIdentifier::AES_CTR:
case CryptoAlgorithmIdentifier::AES_CBC:
case CryptoAlgorithmIdentifier::AES_CMAC:
case CryptoAlgorithmIdentifier::AES_GCM:
case CryptoAlgorithmIdentifier::AES_CFB:
return createAesKeyGenParams(exec, value);
case CryptoAlgorithmIdentifier::HMAC:
return createHmacKeyParams(exec, value);
case CryptoAlgorithmIdentifier::DH:
case CryptoAlgorithmIdentifier::SHA_1:
case CryptoAlgorithmIdentifier::SHA_224:
Expand Down

0 comments on commit f656051

Please sign in to comment.