Add support for serialising verifiable credentials
with password-based encryption
This commit is contained in:
Родитель
30e572634e
Коммит
ebbb771f3f
|
@ -7,6 +7,13 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
2F23154D27F708A000A35917 /* HmacSha2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F23154C27F708A000A35917 /* HmacSha2.swift */; };
|
||||
2F23154F27F711B400A35917 /* HmacSha2AesCbc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F23154E27F711B400A35917 /* HmacSha2AesCbc.swift */; };
|
||||
2FA3E3CB27F1EA7F0008F7C3 /* EphemeralSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA3E3CA27F1EA7F0008F7C3 /* EphemeralSecret.swift */; };
|
||||
2FA3E3CF27F1F36C0008F7C3 /* Pbkdf.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA3E3CE27F1F36C0008F7C3 /* Pbkdf.swift */; };
|
||||
2FA3E3D127F208A80008F7C3 /* PbkdfTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA3E3D027F208A80008F7C3 /* PbkdfTests.swift */; };
|
||||
2FD7F10C27F3588100B2C05A /* Aes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FD7F10B27F3588100B2C05A /* Aes.swift */; };
|
||||
2FD7F10E27F3673500B2C05A /* AesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FD7F10D27F3673500B2C05A /* AesTests.swift */; };
|
||||
551F3059252E378E0081D5E7 /* CryptoOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551F3058252E378E0081D5E7 /* CryptoOperations.swift */; };
|
||||
553D4AAF280F443A00FD39A6 /* VCSDKConfigurable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553D4AAE280F443A00FD39A6 /* VCSDKConfigurable.swift */; };
|
||||
5540903D25000F6A001246DB /* Signing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5540903C25000F6A001246DB /* Signing.swift */; };
|
||||
|
@ -60,6 +67,13 @@
|
|||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
2F23154C27F708A000A35917 /* HmacSha2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HmacSha2.swift; sourceTree = "<group>"; };
|
||||
2F23154E27F711B400A35917 /* HmacSha2AesCbc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HmacSha2AesCbc.swift; sourceTree = "<group>"; };
|
||||
2FA3E3CA27F1EA7F0008F7C3 /* EphemeralSecret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EphemeralSecret.swift; sourceTree = "<group>"; };
|
||||
2FA3E3CE27F1F36C0008F7C3 /* Pbkdf.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pbkdf.swift; sourceTree = "<group>"; };
|
||||
2FA3E3D027F208A80008F7C3 /* PbkdfTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PbkdfTests.swift; sourceTree = "<group>"; };
|
||||
2FD7F10B27F3588100B2C05A /* Aes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Aes.swift; sourceTree = "<group>"; };
|
||||
2FD7F10D27F3673500B2C05A /* AesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AesTests.swift; sourceTree = "<group>"; };
|
||||
551F3058252E378E0081D5E7 /* CryptoOperations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoOperations.swift; sourceTree = "<group>"; };
|
||||
553D4AAE280F443A00FD39A6 /* VCSDKConfigurable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCSDKConfigurable.swift; sourceTree = "<group>"; };
|
||||
5540903C25000F6A001246DB /* Signing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Signing.swift; sourceTree = "<group>"; };
|
||||
|
@ -134,6 +148,7 @@
|
|||
92CDE80524E1ABEA00F95B5D /* Random32BytesSecret.swift */,
|
||||
92CDE80724E1F7AF00F95B5D /* Secret.swift */,
|
||||
928AAF7024EEDC9D0029A8F9 /* Secp256k1PublicKey.swift */,
|
||||
2FA3E3CA27F1EA7F0008F7C3 /* EphemeralSecret.swift */,
|
||||
);
|
||||
path = Keys;
|
||||
sourceTree = "<group>";
|
||||
|
@ -186,6 +201,8 @@
|
|||
928AAF7224EEE0AB0029A8F9 /* Secp256k1PublicKeyTests.swift */,
|
||||
92CDE7E724DE1A2000F95B5D /* KeychainSecretStoreTests.swift */,
|
||||
92CDE80B24E213AE00F95B5D /* Random32BytesSecretTests.swift */,
|
||||
2FA3E3D027F208A80008F7C3 /* PbkdfTests.swift */,
|
||||
2FD7F10D27F3673500B2C05A /* AesTests.swift */,
|
||||
);
|
||||
path = VCCryptoTests;
|
||||
sourceTree = "<group>";
|
||||
|
@ -207,6 +224,10 @@
|
|||
92CDE7DA24DDC65100F95B5D /* Sha512.swift */,
|
||||
55ADE29824F5752500D9990E /* Sha256.swift */,
|
||||
5540903C25000F6A001246DB /* Signing.swift */,
|
||||
2FA3E3CE27F1F36C0008F7C3 /* Pbkdf.swift */,
|
||||
2FD7F10B27F3588100B2C05A /* Aes.swift */,
|
||||
2F23154C27F708A000A35917 /* HmacSha2.swift */,
|
||||
2F23154E27F711B400A35917 /* HmacSha2AesCbc.swift */,
|
||||
);
|
||||
path = Algo;
|
||||
sourceTree = "<group>";
|
||||
|
@ -402,11 +423,15 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
2FA3E3CF27F1F36C0008F7C3 /* Pbkdf.swift in Sources */,
|
||||
92CDE7DB24DDC65100F95B5D /* Sha512.swift in Sources */,
|
||||
92CDE7C924DCA48A00F95B5D /* SecretStoring.swift in Sources */,
|
||||
551F3059252E378E0081D5E7 /* CryptoOperations.swift in Sources */,
|
||||
92CDE7E624DE109200F95B5D /* Data+Base64.swift in Sources */,
|
||||
2F23154D27F708A000A35917 /* HmacSha2.swift in Sources */,
|
||||
92CDE7CB24DCA61800F95B5D /* KeychainSecretStore.swift in Sources */,
|
||||
2FD7F10C27F3588100B2C05A /* Aes.swift in Sources */,
|
||||
2FA3E3CB27F1EA7F0008F7C3 /* EphemeralSecret.swift in Sources */,
|
||||
92CDE7DE24DDCB9500F95B5D /* Data+Hex.swift in Sources */,
|
||||
92CDE80624E1ABEA00F95B5D /* Random32BytesSecret.swift in Sources */,
|
||||
92CDE7D924DDBA0500F95B5D /* HmacSha512.swift in Sources */,
|
||||
|
@ -416,6 +441,7 @@
|
|||
92CDE7D224DCA6CF00F95B5D /* Secp256k1.swift in Sources */,
|
||||
928AAF7124EEDC9D0029A8F9 /* Secp256k1PublicKey.swift in Sources */,
|
||||
5540903D25000F6A001246DB /* Signing.swift in Sources */,
|
||||
2F23154F27F711B400A35917 /* HmacSha2AesCbc.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -425,6 +451,8 @@
|
|||
files = (
|
||||
928AAF7324EEE0AB0029A8F9 /* Secp256k1PublicKeyTests.swift in Sources */,
|
||||
92CDE7D724DCD07D00F95B5D /* Secp256k1Tests.swift in Sources */,
|
||||
2FD7F10E27F3673500B2C05A /* AesTests.swift in Sources */,
|
||||
2FA3E3D127F208A80008F7C3 /* PbkdfTests.swift in Sources */,
|
||||
92CDE7D524DCCE3C00F95B5D /* SecretStoreMock.swift in Sources */,
|
||||
92CDE80A24E2047900F95B5D /* SecretMock.swift in Sources */,
|
||||
92CDE80C24E213AE00F95B5D /* Random32BytesSecretTests.swift in Sources */,
|
||||
|
|
|
@ -0,0 +1,159 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Foundation
|
||||
import CommonCrypto
|
||||
|
||||
enum AesError : Error {
|
||||
case keyWrapError
|
||||
case keyUnwrapError
|
||||
case invalidSecret
|
||||
case cryptoError(operation:CCOperation, status:CCCryptorStatus)
|
||||
}
|
||||
|
||||
public struct Aes {
|
||||
|
||||
private let keyWrapAlg = CCWrappingAlgorithm(kCCWRAPAES)
|
||||
|
||||
public init() {}
|
||||
|
||||
public func wrap(key: VCCryptoSecret, with kek: VCCryptoSecret) throws -> Data {
|
||||
|
||||
// Look for an early out
|
||||
guard key is Secret, kek is Secret else {
|
||||
throw AesError.invalidSecret
|
||||
}
|
||||
var wrappedSize: Int = 0
|
||||
try (key as! Secret).withUnsafeBytes { (keyPtr: UnsafeRawBufferPointer) in
|
||||
let keySize = keyPtr.bindMemory(to: UInt8.self).count
|
||||
wrappedSize = CCSymmetricWrappedSize(keyWrapAlg, keySize)
|
||||
}
|
||||
var wrapped = Data(repeating: 0, count: wrappedSize)
|
||||
try wrapped.withUnsafeMutableBytes({ (wrappedPtr: UnsafeMutableRawBufferPointer) in
|
||||
let wrappedBytes = wrappedPtr.bindMemory(to: UInt8.self)
|
||||
|
||||
try (key as! Secret).withUnsafeBytes { (keyPtr: UnsafeRawBufferPointer) in
|
||||
let keyBytes = keyPtr.bindMemory(to: UInt8.self)
|
||||
|
||||
try (kek as! Secret).withUnsafeBytes { (kekPtr: UnsafeRawBufferPointer) in
|
||||
let kekBytes = kekPtr.bindMemory(to: UInt8.self)
|
||||
let status = CCSymmetricKeyWrap(keyWrapAlg,
|
||||
CCrfc3394_iv,
|
||||
CCrfc3394_ivLen,
|
||||
kekBytes.baseAddress,
|
||||
kekBytes.count,
|
||||
keyBytes.baseAddress,
|
||||
keyBytes.count,
|
||||
wrappedBytes.baseAddress,
|
||||
&wrappedSize)
|
||||
guard status == kCCSuccess else {
|
||||
throw AesError.keyWrapError
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return wrapped
|
||||
}
|
||||
|
||||
public func unwrap(wrapped: Data, using kek: VCCryptoSecret) throws -> VCCryptoSecret {
|
||||
|
||||
// Look for an early out
|
||||
guard kek is Secret else {
|
||||
throw AesError.invalidSecret
|
||||
}
|
||||
|
||||
var unwrappedSize = CCSymmetricUnwrappedSize(keyWrapAlg, wrapped.count)
|
||||
var unwrapped = Data(repeating: 0, count: unwrappedSize)
|
||||
defer {
|
||||
unwrapped.withUnsafeMutableBytes { (unwrappedPtr) in
|
||||
memset_s(unwrappedPtr.baseAddress, unwrappedSize, 0, unwrappedSize)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
try unwrapped.withUnsafeMutableBytes { (unwrappedPtr: UnsafeMutableRawBufferPointer) in
|
||||
let unwrappedBytes = unwrappedPtr.bindMemory(to: UInt8.self)
|
||||
|
||||
try wrapped.withUnsafeBytes { (wrappedPtr: UnsafeRawBufferPointer) in
|
||||
let wrappedBytes = wrappedPtr.bindMemory(to: UInt8.self)
|
||||
|
||||
try (kek as! Secret).withUnsafeBytes { (kekPtr: UnsafeRawBufferPointer) in
|
||||
let kekBytes = kekPtr.bindMemory(to: UInt8.self)
|
||||
let status = CCSymmetricKeyUnwrap(keyWrapAlg,
|
||||
CCrfc3394_iv,
|
||||
CCrfc3394_ivLen,
|
||||
kekBytes.baseAddress,
|
||||
kekBytes.count,
|
||||
wrappedBytes.baseAddress,
|
||||
wrappedBytes.count,
|
||||
unwrappedBytes.baseAddress,
|
||||
&unwrappedSize)
|
||||
guard status == kCCSuccess else {
|
||||
throw AesError.keyUnwrapError
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return EphemeralSecret(with: unwrapped)
|
||||
}
|
||||
|
||||
public func encrypt(data: Data, with key: VCCryptoSecret, iv: Data) throws -> Data {
|
||||
|
||||
// If the input isn't perfectly aligned to the AES block size, then padding is required
|
||||
let options = data.count % blockSize == 0 ? CCOptions(0) : CCOptions(kCCOptionPKCS7Padding)
|
||||
return try self.apply(operation: CCOperation(kCCEncrypt), withOptions: options, to: data, using: key, iv: iv)
|
||||
}
|
||||
|
||||
public func decrypt(data: Data, with key: VCCryptoSecret, iv: Data) throws -> Data {
|
||||
return try self.apply(operation: CCOperation(kCCDecrypt), withOptions: CCOptions(kCCOptionPKCS7Padding), to: data, using: key, iv: iv)
|
||||
}
|
||||
|
||||
// AES processes inputs in blocks of 128 bits (16 bytes)
|
||||
internal let blockSize = size_t(16)
|
||||
|
||||
private func apply(operation: CCOperation, withOptions options:CCOptions, to data: Data, using key: VCCryptoSecret, iv: Data) throws -> Data {
|
||||
|
||||
// Look for an early out
|
||||
guard key is Secret else { throw AesError.invalidSecret }
|
||||
|
||||
// Allocate the output buffer
|
||||
var outputSize = size_t(data.count)
|
||||
let modulo = data.count % blockSize
|
||||
if modulo > 0 {
|
||||
outputSize += size_t(blockSize - modulo)
|
||||
}
|
||||
var output = [UInt8](repeating: 0, count: outputSize)
|
||||
|
||||
try (key as! Secret).withUnsafeBytes { (keyPtr: UnsafeRawBufferPointer) in
|
||||
let keyBytes = keyPtr.bindMemory(to: UInt8.self)
|
||||
|
||||
try iv.withUnsafeBytes { (ivPtr: UnsafeRawBufferPointer) in
|
||||
let ivBytes = ivPtr.bindMemory(to: UInt8.self)
|
||||
|
||||
try data.withUnsafeBytes { (dataPtr: UnsafeRawBufferPointer) in
|
||||
let dataBytes = dataPtr.bindMemory(to: UInt8.self)
|
||||
|
||||
let status = CCCrypt(operation,
|
||||
CCAlgorithm(kCCAlgorithmAES),
|
||||
options,
|
||||
keyBytes.baseAddress,
|
||||
keyBytes.count,
|
||||
ivBytes.baseAddress,
|
||||
dataBytes.baseAddress,
|
||||
dataBytes.count,
|
||||
&output,
|
||||
outputSize,
|
||||
&outputSize)
|
||||
if status == kCCSuccess {
|
||||
output.removeSubrange(outputSize..<output.count)
|
||||
} else {
|
||||
throw AesError.cryptoError(operation:operation, status:status)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Data(output)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Foundation
|
||||
import CommonCrypto
|
||||
|
||||
enum HmacSha2Error: Error {
|
||||
case invalidMessage
|
||||
case invalidSecret
|
||||
case invalidAlgorithm
|
||||
}
|
||||
|
||||
public struct HmacSha2 {
|
||||
|
||||
private let macSize: Int
|
||||
private let algorithm: UInt32
|
||||
|
||||
public init(algorithm: UInt32) throws {
|
||||
switch (algorithm) {
|
||||
case UInt32(kCCHmacAlgSHA256):
|
||||
self.macSize = Int(CC_SHA256_DIGEST_LENGTH)
|
||||
case UInt32(kCCHmacAlgSHA384):
|
||||
self.macSize = Int(CC_SHA384_DIGEST_LENGTH)
|
||||
case UInt32(kCCHmacAlgSHA512):
|
||||
self.macSize = Int(CC_SHA512_DIGEST_LENGTH)
|
||||
default:
|
||||
throw HmacSha2Error.invalidAlgorithm
|
||||
}
|
||||
self.algorithm = algorithm
|
||||
}
|
||||
|
||||
/// Authenticate a message
|
||||
/// - Parameters:
|
||||
/// - message: The message to authenticate
|
||||
/// - secret: The secret used for authentication
|
||||
/// - Returns: The authentication code for the message
|
||||
public func authenticate(message: Data, with secret: VCCryptoSecret) throws -> Data {
|
||||
|
||||
// Look for an early out
|
||||
guard message.count > 0 else { throw HmacSha2Error.invalidMessage }
|
||||
guard secret is Secret else { throw HmacSha2Error.invalidSecret }
|
||||
|
||||
// Apply
|
||||
let ccHmacAlg = self.algorithm
|
||||
var mac: [UInt8] = [UInt8](repeating: 0, count:self.macSize)
|
||||
try message.withUnsafeBytes { (messagePtr: UnsafeRawBufferPointer) in
|
||||
let messageBytes = messagePtr.bindMemory(to: UInt8.self)
|
||||
|
||||
try (secret as! Secret).withUnsafeBytes { (secretPtr: UnsafeRawBufferPointer) in
|
||||
let secretBytes = secretPtr.bindMemory(to: UInt8.self)
|
||||
|
||||
CCHmac(ccHmacAlg, secretBytes.baseAddress, secretBytes.count, messageBytes.baseAddress, messageBytes.count, &mac)
|
||||
}
|
||||
}
|
||||
return Data(mac)
|
||||
}
|
||||
|
||||
/// Verify that the authentication code is valid
|
||||
/// - Parameters:
|
||||
/// - mac: The authentication code
|
||||
/// - message: The message
|
||||
/// - secret: The secret used
|
||||
/// - Returns: True if the authentication code is valid
|
||||
public func validate(_ mac: Data, authenticating message: Data, with secret: VCCryptoSecret) throws -> Bool {
|
||||
|
||||
let authentication = try self.authenticate(message: message, with: secret)
|
||||
return mac == authentication
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Foundation
|
||||
import CommonCrypto
|
||||
|
||||
enum HmacSha2AesCbcError: Error {
|
||||
case unsupportedMethod(name: String?)
|
||||
case invalidAuthenticationCode
|
||||
}
|
||||
|
||||
public struct HmacSha2AesCbc {
|
||||
|
||||
private let aes: Aes
|
||||
private let hmac: HmacSha2
|
||||
|
||||
public init(methodName: String) throws {
|
||||
|
||||
let (hmacAlg, _) = try HmacSha2AesCbc.props(for: methodName)
|
||||
self.hmac = try HmacSha2(algorithm: UInt32(hmacAlg))
|
||||
self.aes = Aes()
|
||||
}
|
||||
|
||||
public func encrypt(plainText: Data, using aad: Data, iv: Data, with keys: (mac: EphemeralSecret, enc: EphemeralSecret)) throws -> (Data, Data) {
|
||||
|
||||
let cipherText = try aes.encrypt(data: plainText, with: keys.enc, iv: iv)
|
||||
let mac = try self.authenticate(aad: aad, iv: iv, cipherText: cipherText, with: keys.mac)
|
||||
return (cipherText, mac)
|
||||
}
|
||||
|
||||
public func decrypt(_ input: (cipherText: Data, mac: Data), using aad: Data, iv: Data, with keys: (mac: EphemeralSecret, enc: EphemeralSecret)) throws -> Data {
|
||||
|
||||
// Validate the message authentication code
|
||||
let mac = try self.authenticate(aad: aad, iv: iv, cipherText: input.cipherText, with: keys.mac)
|
||||
if input.mac == mac {
|
||||
return try aes.decrypt(data: input.cipherText, with: keys.enc, iv: iv)
|
||||
}
|
||||
throw HmacSha2AesCbcError.invalidAuthenticationCode
|
||||
}
|
||||
|
||||
public static func props(for methodName: String) throws -> (Int, Int) {
|
||||
|
||||
let result: (hmacAlg: Int, keySize: Int)
|
||||
switch methodName {
|
||||
case "A128CBC-HS256":
|
||||
result.hmacAlg = kCCHmacAlgSHA256
|
||||
result.keySize = 16
|
||||
case "A192CBC-HS384":
|
||||
result.hmacAlg = kCCHmacAlgSHA384
|
||||
result.keySize = 24
|
||||
case "A256CBC-HS512":
|
||||
result.hmacAlg = kCCHmacAlgSHA512
|
||||
result.keySize = 32
|
||||
default:
|
||||
throw HmacSha2AesCbcError.unsupportedMethod(name: methodName)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private func authenticate(aad: Data, iv: Data, cipherText: Data, with key: EphemeralSecret) throws -> Data {
|
||||
|
||||
// Compose the message from which to generate the MAC
|
||||
let bitCount = UInt64(8 * aad.count).bigEndian
|
||||
let al = withUnsafeBytes(of: bitCount, Array.init)
|
||||
var message: Data = aad
|
||||
message.append(iv)
|
||||
message.append(cipherText)
|
||||
message.append(contentsOf: al)
|
||||
|
||||
// Apply, and truncate
|
||||
let mac = try hmac.authenticate(message: message, with: key)
|
||||
return mac.prefix(key.value.count)
|
||||
}
|
||||
}
|
|
@ -6,12 +6,9 @@
|
|||
import Foundation
|
||||
import CommonCrypto
|
||||
|
||||
enum HmacSha512Error: Error {
|
||||
case invalidMessage
|
||||
case invalidSecret
|
||||
}
|
||||
|
||||
public struct HmacSha512 {
|
||||
|
||||
public init() { }
|
||||
|
||||
/// Authenticate a message
|
||||
/// - Parameters:
|
||||
|
@ -19,17 +16,8 @@ public struct HmacSha512 {
|
|||
/// - secret: The secret used for authentication
|
||||
/// - Returns: The authentication code for the message
|
||||
public func authenticate(message: Data, withSecret secret: VCCryptoSecret) throws -> Data {
|
||||
guard message.count > 0 else { throw HmacSha512Error.invalidMessage }
|
||||
guard secret is Secret else { throw HmacSha512Error.invalidSecret }
|
||||
|
||||
var messageAuthCode : [UInt8] = [UInt8](repeating: 0, count:Int(CC_SHA512_DIGEST_LENGTH))
|
||||
try (secret as! Secret).withUnsafeBytes { (secretPtr) in
|
||||
message.withUnsafeBytes {
|
||||
CCHmac(UInt32(kCCHmacAlgSHA512), secretPtr.bindMemory(to: UInt8.self).baseAddress!, secretPtr.count, $0.baseAddress, message.count, &messageAuthCode)
|
||||
}
|
||||
}
|
||||
|
||||
return Data(messageAuthCode)
|
||||
let hmac = try HmacSha2(algorithm: UInt32(kCCHmacAlgSHA512))
|
||||
return try hmac.authenticate(message: message, with: secret)
|
||||
}
|
||||
|
||||
/// Verify that the authentication code is valid
|
||||
|
@ -39,7 +27,7 @@ public struct HmacSha512 {
|
|||
/// - secret: The secret used
|
||||
/// - Returns: True if the authentication code is valid
|
||||
public func isValidAuthenticationCode(_ mac: Data, authenticating message: Data, withSecret secret: VCCryptoSecret) throws -> Bool {
|
||||
let authCode = try self.authenticate(message: message, withSecret: secret)
|
||||
return mac == authCode
|
||||
let hmac = try HmacSha2(algorithm: UInt32(kCCHmacAlgSHA512))
|
||||
return try hmac.validate(mac, authenticating: message, with: secret)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
//
|
||||
// Pbes2HmacSha2Aes.swift
|
||||
// VCCrypto
|
||||
//
|
||||
// Created by Stephen Higgins on 01/04/2022.
|
||||
// Copyright © 2022 Daniel Godbout. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
|
@ -0,0 +1,73 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Foundation
|
||||
import CommonCrypto
|
||||
|
||||
enum PbkdfError : Error {
|
||||
case keyDerivationError
|
||||
case invalidAlgorithmNameError(name:String)
|
||||
}
|
||||
|
||||
public struct Pbkdf {
|
||||
|
||||
public init() { }
|
||||
|
||||
public func derive(from password: String, withSaltInput p2s: Data, forAlgorithm algorithm: String, rounds: UInt32) throws -> VCCryptoSecret {
|
||||
|
||||
// Determine the algorithm, and key size
|
||||
let size: size_t, pseudoRandomAlgorithm: CCPseudoRandomAlgorithm
|
||||
switch (algorithm) {
|
||||
case "PBES2-HS256+A128KW":
|
||||
pseudoRandomAlgorithm = CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256)
|
||||
size = 16
|
||||
case "PBES2-HS384+A192KW":
|
||||
pseudoRandomAlgorithm = CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA384)
|
||||
size = 24
|
||||
case "PBES2-HS512+A256KW":
|
||||
pseudoRandomAlgorithm = CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA512)
|
||||
size = 32
|
||||
default:
|
||||
throw PbkdfError.invalidAlgorithmNameError(name:algorithm)
|
||||
}
|
||||
|
||||
// Construct the salt, c.f. https://datatracker.ietf.org/doc/html/rfc7517#appendix-C.4
|
||||
guard var salt = algorithm.data(using: .utf8) else {
|
||||
throw PbkdfError.invalidAlgorithmNameError(name:algorithm)
|
||||
}
|
||||
salt.append(UInt8(0))
|
||||
salt.append(p2s)
|
||||
|
||||
// Derive the key
|
||||
var derived = Data(count: size)
|
||||
defer {
|
||||
derived.withUnsafeMutableBytes { (derivedPtr) in
|
||||
memset_s(derivedPtr.baseAddress, size, 0, size)
|
||||
return
|
||||
}
|
||||
}
|
||||
try derived.withUnsafeMutableBytes { (derivedPtr: UnsafeMutableRawBufferPointer) in
|
||||
let derivedBytes = derivedPtr.bindMemory(to: UInt8.self)
|
||||
|
||||
try salt.withUnsafeBytes { (saltPtr: UnsafeRawBufferPointer) in
|
||||
let saltBytes = saltPtr.bindMemory(to: UInt8.self)
|
||||
|
||||
let status = CCKeyDerivationPBKDF(CCPBKDFAlgorithm(kCCPBKDF2),
|
||||
password,
|
||||
password.utf8.count,
|
||||
saltBytes.baseAddress,
|
||||
saltBytes.count,
|
||||
pseudoRandomAlgorithm,
|
||||
rounds,
|
||||
derivedBytes.baseAddress,
|
||||
derivedBytes.count)
|
||||
guard status == kCCSuccess else {
|
||||
throw PbkdfError.keyDerivationError
|
||||
}
|
||||
}
|
||||
}
|
||||
return EphemeralSecret(with: derived)
|
||||
}
|
||||
}
|
|
@ -6,6 +6,8 @@
|
|||
import Foundation
|
||||
import Secp256k1
|
||||
|
||||
public typealias Secp256k1KeyPair = (publicKey: Secp256k1PublicKey, privateKey: VCCryptoSecret)
|
||||
|
||||
enum Secp256k1Error: Error {
|
||||
case invalidMessageHash
|
||||
case invalidSecretKey
|
||||
|
|
|
@ -4,13 +4,16 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
public protocol CryptoOperating {
|
||||
var secretStore: SecretStoring { get }
|
||||
|
||||
func generateKey() throws -> VCCryptoSecret
|
||||
func retrieveKeyFromStorage(withId id: UUID) -> VCCryptoSecret
|
||||
func retrieveKeyIfStored(uuid: UUID) throws -> VCCryptoSecret?
|
||||
}
|
||||
|
||||
public struct CryptoOperations: CryptoOperating {
|
||||
|
||||
private let secretStore: SecretStoring
|
||||
public let secretStore: SecretStoring
|
||||
|
||||
private let sdkConfiguration: VCSDKConfigurable
|
||||
|
||||
|
@ -33,4 +36,21 @@ public struct CryptoOperations: CryptoOperating {
|
|||
let accessGroup = sdkConfiguration.accessGroupIdentifier
|
||||
return Random32BytesSecret(withStore: secretStore, andId: id, inAccessGroup: accessGroup)
|
||||
}
|
||||
|
||||
/// Tests if a key corresponding to the given id is stored and, if it is, returns a reference to it; returns nil otherwise
|
||||
public func retrieveKeyIfStored(uuid: UUID) throws -> VCCryptoSecret? {
|
||||
|
||||
let accessGroup = sdkConfiguration.accessGroupIdentifier
|
||||
var keyRef: VCCryptoSecret? = nil
|
||||
do {
|
||||
let _ = try secretStore.getSecret(id: uuid,
|
||||
itemTypeCode: Random32BytesSecret.itemTypeCode,
|
||||
accessGroup: accessGroup)
|
||||
keyRef = Random32BytesSecret(withStore: secretStore, andId: uuid)
|
||||
}
|
||||
catch SecretStoringError.itemNotFound {
|
||||
keyRef = nil
|
||||
}
|
||||
return keyRef
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,11 +8,7 @@ import Foundation
|
|||
public enum KeychainStoreError: Error {
|
||||
case deleteFromStoreError(status: OSStatus)
|
||||
case saveToStoreError(status: Int32)
|
||||
case itemNotFound
|
||||
case readFromStoreError(status: OSStatus)
|
||||
case invalidItemInStore
|
||||
case itemAlreadyInStore
|
||||
case invalidType
|
||||
}
|
||||
|
||||
struct KeychainSecretStore : SecretStoring {
|
||||
|
@ -48,9 +44,9 @@ struct KeychainSecretStore : SecretStoring {
|
|||
|
||||
var item: CFTypeRef?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
||||
guard status != errSecItemNotFound else { throw KeychainStoreError.itemNotFound }
|
||||
guard status != errSecItemNotFound else { throw SecretStoringError.itemNotFound }
|
||||
guard status == errSecSuccess else { throw KeychainStoreError.readFromStoreError(status: status as OSStatus) }
|
||||
guard var value = item as? Data else { throw KeychainStoreError.invalidItemInStore }
|
||||
guard var value = item as? Data else { throw SecretStoringError.invalidItemInStore }
|
||||
defer {
|
||||
let secretSize = value.count
|
||||
value.withUnsafeMutableBytes { (secretPtr) in
|
||||
|
@ -78,7 +74,7 @@ struct KeychainSecretStore : SecretStoring {
|
|||
}
|
||||
}
|
||||
|
||||
guard itemTypeCode.count == 4 else { throw KeychainStoreError.invalidType }
|
||||
guard itemTypeCode.count == 4 else { throw SecretStoringError.invalidType }
|
||||
|
||||
// kSecAttrAccount is used to store the secret Id so that we can look it up later
|
||||
// kSecAttrService is always set to vcService to enable us to lookup all our secrets later if needed
|
||||
|
@ -116,7 +112,7 @@ struct KeychainSecretStore : SecretStoring {
|
|||
/// - accessGroup: The access group of the secret.
|
||||
func deleteSecret(id: UUID, itemTypeCode: String, accessGroup: String? = nil) throws {
|
||||
|
||||
guard itemTypeCode.count == 4 else { throw KeychainStoreError.invalidType }
|
||||
guard itemTypeCode.count == 4 else { throw SecretStoringError.invalidType }
|
||||
|
||||
// kSecAttrAccount is used to store the secret Id so that we can look it up later
|
||||
// kSecAttrService is always set to vcService to enable us to lookup all our secrets later if needed
|
||||
|
@ -133,8 +129,52 @@ struct KeychainSecretStore : SecretStoring {
|
|||
}
|
||||
|
||||
let status = SecItemDelete(query as CFDictionary)
|
||||
guard status == errSecSuccess else {
|
||||
guard status == errSecSuccess || status == errSecItemNotFound else {
|
||||
throw KeychainStoreError.deleteFromStoreError(status: status)
|
||||
}
|
||||
}
|
||||
|
||||
/// Save the secret to keychain
|
||||
/// - Parameters:
|
||||
/// - secret: the secret container
|
||||
func save(secret: VCCryptoSecret) throws {
|
||||
|
||||
let (ephemeral, itemTypeCode) = try Self.secretDataAndItemTypeCodeFor(secret: secret)
|
||||
var data = Data()
|
||||
data.append(ephemeral.value)
|
||||
try self.saveSecret(id: secret.id,
|
||||
itemTypeCode: itemTypeCode,
|
||||
accessGroup: secret.accessGroup,
|
||||
value: &data)
|
||||
}
|
||||
|
||||
/// Remove a secret from the keychain
|
||||
/// - Parameters:
|
||||
/// - secret: the secret reference
|
||||
func delete(secret: VCCryptoSecret) throws {
|
||||
|
||||
let (_, itemTypeCode) = try Self.secretDataAndItemTypeCodeFor(secret: secret)
|
||||
try self.deleteSecret(id: secret.id,
|
||||
itemTypeCode: itemTypeCode,
|
||||
accessGroup: secret.accessGroup)
|
||||
}
|
||||
|
||||
private static func secretDataAndItemTypeCodeFor(secret: VCCryptoSecret) throws -> (EphemeralSecret, String) {
|
||||
|
||||
// Get out the secret data
|
||||
let ephemeral = try EphemeralSecret(with: secret)
|
||||
|
||||
// Figure out the item type code
|
||||
var itemTypeCode: String = ""
|
||||
if let internalSecret = secret as? Secret {
|
||||
itemTypeCode = type(of: internalSecret).self.itemTypeCode
|
||||
}
|
||||
if itemTypeCode == "" {
|
||||
// Fallback
|
||||
itemTypeCode = String(format: "r%02dB", ephemeral.value.count)
|
||||
}
|
||||
|
||||
// Wrap up and return
|
||||
return (ephemeral, itemTypeCode)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Foundation
|
||||
|
||||
/// An ephemeral secret is one that vanishes into thin air when you are not looking at it
|
||||
/// (i.e. when it goes out of scope)
|
||||
public final class EphemeralSecret: Secret {
|
||||
|
||||
private enum EphemeralSecretError: Error {
|
||||
case secRandomCopyBytesFailed(status: OSStatus)
|
||||
}
|
||||
|
||||
static var itemTypeCode: String = ""
|
||||
|
||||
public var accessGroup: String? = nil
|
||||
|
||||
public private(set) var id = UUID()
|
||||
public private(set) var value: Data
|
||||
|
||||
public init(with data: Data, id: UUID? = nil, accessGroup: String? = nil) {
|
||||
|
||||
// Since we practively zero out the contents of the `value` buffer
|
||||
// on deallocation, we make a separate copy of the input here
|
||||
value = Data()
|
||||
value.append(data)
|
||||
if let uuid = id {
|
||||
self.id = uuid
|
||||
}
|
||||
self.accessGroup = accessGroup
|
||||
}
|
||||
|
||||
public init(size: Int = 32) throws {
|
||||
value = Data(count:size)
|
||||
let result = value.withUnsafeMutableBytes { (secretPtr) in
|
||||
SecRandomCopyBytes(kSecRandomDefault, secretPtr.count, secretPtr.baseAddress!)
|
||||
}
|
||||
guard result == errSecSuccess else {
|
||||
throw EphemeralSecretError.secRandomCopyBytesFailed(status: result)
|
||||
}
|
||||
}
|
||||
|
||||
public init(with secret: VCCryptoSecret) throws {
|
||||
value = Data()
|
||||
try (secret as! Secret).withUnsafeBytes { secretPtr in
|
||||
value.append(secretPtr.bindMemory(to: UInt8.self))
|
||||
}
|
||||
self.id = secret.id
|
||||
self.accessGroup = secret.accessGroup
|
||||
}
|
||||
|
||||
public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> Void) throws {
|
||||
try value.withUnsafeBytes { (valuePtr) in
|
||||
try body(valuePtr)
|
||||
}
|
||||
}
|
||||
|
||||
public func isValidKey() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public func migrateKey(fromAccessGroup currentAccessGroup: String?) throws {
|
||||
self.accessGroup
|
||||
}
|
||||
|
||||
public func prefix(_ maxLength: Int) -> EphemeralSecret {
|
||||
return EphemeralSecret(with: value.prefix(maxLength))
|
||||
}
|
||||
|
||||
public func suffix(_ maxLength: Int) -> EphemeralSecret {
|
||||
return EphemeralSecret(with: value.suffix(maxLength))
|
||||
}
|
||||
|
||||
deinit {
|
||||
value.withUnsafeMutableBytes { (secretPtr) in
|
||||
let secretBytes = secretPtr.bindMemory(to: UInt8.self)
|
||||
memset_s(secretBytes.baseAddress, secretBytes.count, 0, secretBytes.count)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,10 +5,21 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public enum SecretStoringError: Error {
|
||||
case itemNotFound
|
||||
case invalidItemInStore
|
||||
case itemAlreadyInStore
|
||||
case invalidType
|
||||
case invalidSecret
|
||||
}
|
||||
|
||||
// public until Identifier Creation is implemented.
|
||||
public protocol SecretStoring {
|
||||
|
||||
func getSecret(id: UUID, itemTypeCode: String, accessGroup: String?) throws -> Data
|
||||
func saveSecret(id: UUID, itemTypeCode: String, accessGroup: String?, value: inout Data) throws
|
||||
func deleteSecret(id: UUID, itemTypeCode: String, accessGroup: String?) throws
|
||||
|
||||
func save(secret:VCCryptoSecret) throws
|
||||
func delete(secret:VCCryptoSecret) throws
|
||||
}
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import XCTest
|
||||
import Foundation
|
||||
|
||||
@testable import VCCrypto
|
||||
|
||||
class AesTests: XCTestCase {
|
||||
|
||||
private var fixture: Aes!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
self.fixture = Aes()
|
||||
}
|
||||
|
||||
/// A sample 'content encryption key' (c.f. rfc3394)
|
||||
private let cek = EphemeralSecret(with: Data(hexString: "00112233445566778899AABBCCDDEEFF000102030405060708090A0B0C0D0E0F"))
|
||||
|
||||
/// A sample 'key encrypting key'
|
||||
private let kek = EphemeralSecret(with: Data(hexString: "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F"))
|
||||
|
||||
/// The sample CEK wrapped with the KEK
|
||||
private let wrapped = Data(hexString: "28C9F404C4B810F4CBCCB35CFB87F8263F5786E2D80ED326CBC7F0E71A99F43BFB988B9B7A02DD21")
|
||||
|
||||
func testKeyWrap() throws {
|
||||
|
||||
let result = try fixture.wrap(key: cek, with: kek)
|
||||
XCTAssertEqual(result, wrapped)
|
||||
}
|
||||
|
||||
func testKeyUnwrap() throws {
|
||||
|
||||
let result = try fixture.unwrap(wrapped: wrapped, using: kek)
|
||||
var data = Data()
|
||||
try (result as! Secret).withUnsafeBytes{ resultPtr in
|
||||
data.append(resultPtr.bindMemory(to: UInt8.self))
|
||||
}
|
||||
XCTAssertEqual(data, cek.value)
|
||||
}
|
||||
|
||||
/// c.f. rfc3602
|
||||
private let key = EphemeralSecret(with: Data(hexString: "56e47a38c5598974bc46903dba290349"))
|
||||
private let iv = Data(hexString: "8ce82eefbea0da3c44699ed7db51b7d9")
|
||||
private let plaintext = Data(hexString: "a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf")
|
||||
private let ciphertext = Data(hexString: "c30e32ffedc0774e6aff6af0869f71aa0f3af07a9a31a9c684db207eb0ef8e4e35907aa632c3ffdf868bb7b29d3d46ad83ce9f9a102ee99d49a53e87f4c3da55")
|
||||
|
||||
func testAESEncryption() throws {
|
||||
|
||||
let result = try fixture.encrypt(data: plaintext, with: key, iv: iv)
|
||||
XCTAssertEqual(result, ciphertext)
|
||||
}
|
||||
|
||||
func testAESDecryption() throws {
|
||||
|
||||
let result = try fixture.decrypt(data: ciphertext, with: key, iv: iv)
|
||||
XCTAssertEqual(result, plaintext)
|
||||
}
|
||||
|
||||
func testAESNonAlignedInput() throws {
|
||||
|
||||
/// 17 bytes of input
|
||||
let plaintext = self.plaintext.prefix(fixture.blockSize+1)
|
||||
let encrypted = try fixture.encrypt(data: plaintext, with: key, iv: iv)
|
||||
let decrypted = try fixture.decrypt(data: encrypted, with: key, iv: iv)
|
||||
|
||||
/// Validate
|
||||
XCTAssertEqual(decrypted, plaintext)
|
||||
}
|
||||
}
|
|
@ -10,7 +10,6 @@ import XCTest
|
|||
class HmacSha512Tests: XCTestCase {
|
||||
|
||||
private let secret = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
private var store: SecretStoreMock!
|
||||
private var hmac: HmacSha512!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
|
@ -35,30 +34,27 @@ class HmacSha512Tests: XCTestCase {
|
|||
}
|
||||
|
||||
func testValidate() throws {
|
||||
let hmac = HmacSha512()
|
||||
let result = try hmac.isValidAuthenticationCode(
|
||||
Data(hexString:"368f91fd6ac70cb1d00035dfa5047a3f258111c7d80650c4de9bf7da20d23ba20aee6f94d3bdf325dc70a2735cf51a910c4364c0802eb4098cdbce813d97df87"),
|
||||
authenticating: Utf8TestData.hello.rawValue.data(using: .utf8)!,
|
||||
withSecret: SecretMock(id: UUID(), withData: secret.data(using: .utf8)!))
|
||||
withSecret: EphemeralSecret(with: secret.data(using: .utf8)!))
|
||||
XCTAssertTrue(result)
|
||||
}
|
||||
|
||||
func testInvalid() throws {
|
||||
let hmac = HmacSha512()
|
||||
let result = try hmac.isValidAuthenticationCode(
|
||||
Data(hexString:"aaaa91fd6ac70cb1d00035dfa5047a3f258111c7d80650c4de9bf7da20d23ba20aee6f94d3bdf325dc70a2735cf51a910c4364c0802eb4098cdbce813d97df87"),
|
||||
authenticating: Utf8TestData.hello.rawValue.data(using: .utf8)!,
|
||||
withSecret: SecretMock(id: UUID(), withData: secret.data(using: .utf8)!))
|
||||
withSecret: EphemeralSecret(with: secret.data(using: .utf8)!))
|
||||
XCTAssertFalse(result)
|
||||
}
|
||||
|
||||
private func runAuthenticateTest(for message: String, expecting expected: String) throws {
|
||||
let expectedResult = Data(hexString:expected)
|
||||
|
||||
let hmac = HmacSha512()
|
||||
let result = try hmac.authenticate(
|
||||
message: message.data(using: .utf8)!,
|
||||
withSecret: SecretMock(id: UUID(), withData: secret.data(using: .utf8)!))
|
||||
withSecret: EphemeralSecret(with: secret.data(using: .utf8)!))
|
||||
XCTAssertEqual(result, expectedResult)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ class KeychainSecretStoreTests: XCTestCase {
|
|||
do {
|
||||
let _ = try store.getSecret(id: secretId, itemTypeCode: "AAAA", accessGroup: nil)
|
||||
XCTAssertTrue(false)
|
||||
} catch KeychainStoreError.itemNotFound {
|
||||
} catch SecretStoringError.itemNotFound {
|
||||
// We expect an exception for this case.
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,4 +21,13 @@ internal class SecretStoreMock: SecretStoring {
|
|||
func deleteSecret(id: UUID, itemTypeCode: String, accessGroup: String?) throws {
|
||||
memoryStore.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
func save(secret: VCCryptoSecret) throws {
|
||||
let ephemeral = try EphemeralSecret(with: secret)
|
||||
memoryStore[secret.id] = ephemeral.value
|
||||
}
|
||||
|
||||
func delete(secret: VCCryptoSecret) throws {
|
||||
memoryStore.removeValue(forKey: secret.id)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import XCTest
|
||||
import Foundation
|
||||
|
||||
@testable import VCCrypto
|
||||
|
||||
class PbkdfTests : XCTestCase {
|
||||
|
||||
func testKeyDeriviation() throws {
|
||||
|
||||
// Setup, c.f. https://datatracker.ietf.org/doc/html/rfc7517#appendix-C.2
|
||||
let password = "Thus from my lips, by yours, my sin is purged."
|
||||
let p2s: [UInt8] = [217, 96, 147, 112, 150, 117, 70, 247, 127, 8, 155, 137, 174, 42, 80, 215]
|
||||
let algorithmName = "PBES2-HS256+A128KW"
|
||||
let expected: [UInt8] = [110, 171, 169, 92, 129, 92, 109, 117, 233, 242, 116, 233, 170, 14, 24, 75]
|
||||
|
||||
// Generate the key
|
||||
let pbkdf = Pbkdf()
|
||||
let derived = try pbkdf.derive(from: password, withSaltInput: Data(p2s), forAlgorithm: algorithmName, rounds: 4096)
|
||||
|
||||
// Validate
|
||||
var data = Data()
|
||||
try (derived as! Secret).withUnsafeBytes { (derivedPtr) in
|
||||
data.append(derivedPtr.bindMemory(to: UInt8.self))
|
||||
}
|
||||
XCTAssertEqual(Data(expected), data)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,17 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
2F357883280983A900997F31 /* VcMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F357882280983A900997F31 /* VcMetadata.swift */; };
|
||||
2F53AA0E27E0CEC800C7C81E /* ProtectedBackupData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F53AA0D27E0CEC800C7C81E /* ProtectedBackupData.swift */; };
|
||||
2F53AA1027E0CF9700C7C81E /* UnprotectedBackupData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F53AA0F27E0CF9700C7C81E /* UnprotectedBackupData.swift */; };
|
||||
2F53AA1227E0E74500C7C81E /* BackupProtectionMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F53AA1127E0E74500C7C81E /* BackupProtectionMethod.swift */; };
|
||||
2F58B92F27FDA78F001C6DFE /* JwePasswordProtectionMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F58B92E27FDA78F001C6DFE /* JwePasswordProtectionMethod.swift */; };
|
||||
2F8CCDAF27FC6FF7004B7861 /* Microsoft2020UnprotectedBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F8CCDAE27FC6FF7004B7861 /* Microsoft2020UnprotectedBackup.swift */; };
|
||||
2F8CCDB327FC7369004B7861 /* WalletMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F8CCDB227FC7369004B7861 /* WalletMetadata.swift */; };
|
||||
2F8CCDB527FC745E004B7861 /* RawIdentity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F8CCDB427FC745E004B7861 /* RawIdentity.swift */; };
|
||||
2F8CCDBD27FC88D3004B7861 /* UnprotectedBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F8CCDBC27FC88D3004B7861 /* UnprotectedBackup.swift */; };
|
||||
2FB0154D2805C75400E67168 /* JwePasswordProtectedBackupData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB0154C2805C75400E67168 /* JwePasswordProtectedBackupData.swift */; };
|
||||
2FD480112817CE99002AF37F /* Microsoft2020IdentifierBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FD480102817CE98002AF37F /* Microsoft2020IdentifierBackup.swift */; };
|
||||
55011E8C2572DF50002D2690 /* VCSDKLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55011E8B2572DF50002D2690 /* VCSDKLog.swift */; };
|
||||
55011E8E2572DFBD002D2690 /* DefaultLogConsumer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55011E8D2572DFBD002D2690 /* DefaultLogConsumer.swift */; };
|
||||
55077ABA25A62DE10052C58D /* IdentifierFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55077AB925A62DE10052C58D /* IdentifierFormatter.swift */; };
|
||||
|
@ -146,6 +157,17 @@
|
|||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
2F357882280983A900997F31 /* VcMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VcMetadata.swift; sourceTree = "<group>"; };
|
||||
2F53AA0D27E0CEC800C7C81E /* ProtectedBackupData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtectedBackupData.swift; sourceTree = "<group>"; };
|
||||
2F53AA0F27E0CF9700C7C81E /* UnprotectedBackupData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnprotectedBackupData.swift; sourceTree = "<group>"; };
|
||||
2F53AA1127E0E74500C7C81E /* BackupProtectionMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupProtectionMethod.swift; sourceTree = "<group>"; };
|
||||
2F58B92E27FDA78F001C6DFE /* JwePasswordProtectionMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JwePasswordProtectionMethod.swift; sourceTree = "<group>"; };
|
||||
2F8CCDAE27FC6FF7004B7861 /* Microsoft2020UnprotectedBackup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Microsoft2020UnprotectedBackup.swift; sourceTree = "<group>"; };
|
||||
2F8CCDB227FC7369004B7861 /* WalletMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletMetadata.swift; sourceTree = "<group>"; };
|
||||
2F8CCDB427FC745E004B7861 /* RawIdentity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawIdentity.swift; sourceTree = "<group>"; };
|
||||
2F8CCDBC27FC88D3004B7861 /* UnprotectedBackup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnprotectedBackup.swift; sourceTree = "<group>"; };
|
||||
2FB0154C2805C75400E67168 /* JwePasswordProtectedBackupData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JwePasswordProtectedBackupData.swift; sourceTree = "<group>"; };
|
||||
2FD480102817CE98002AF37F /* Microsoft2020IdentifierBackup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Microsoft2020IdentifierBackup.swift; sourceTree = "<group>"; };
|
||||
55011E8B2572DF50002D2690 /* VCSDKLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCSDKLog.swift; sourceTree = "<group>"; };
|
||||
55011E8D2572DFBD002D2690 /* DefaultLogConsumer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultLogConsumer.swift; sourceTree = "<group>"; };
|
||||
55077AB925A62DE10052C58D /* IdentifierFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifierFormatter.swift; sourceTree = "<group>"; };
|
||||
|
@ -301,6 +323,56 @@
|
|||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
2F53AA0A27E0CDC000C7C81E /* backup */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2F53AA0C27E0CEA200C7C81E /* content */,
|
||||
2F53AA0B27E0CDCB00C7C81E /* container */,
|
||||
2F8CCDBC27FC88D3004B7861 /* UnprotectedBackup.swift */,
|
||||
);
|
||||
path = backup;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2F53AA0B27E0CDCB00C7C81E /* container */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2F58B92D27FDA77B001C6DFE /* jwe */,
|
||||
2F53AA1127E0E74500C7C81E /* BackupProtectionMethod.swift */,
|
||||
);
|
||||
path = container;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2F53AA0C27E0CEA200C7C81E /* content */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2F8CCDAB27FC6EE9004B7861 /* microsoft2020 */,
|
||||
2F53AA0D27E0CEC800C7C81E /* ProtectedBackupData.swift */,
|
||||
2F53AA0F27E0CF9700C7C81E /* UnprotectedBackupData.swift */,
|
||||
);
|
||||
path = content;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2F58B92D27FDA77B001C6DFE /* jwe */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2F58B92E27FDA78F001C6DFE /* JwePasswordProtectionMethod.swift */,
|
||||
2FB0154C2805C75400E67168 /* JwePasswordProtectedBackupData.swift */,
|
||||
);
|
||||
path = jwe;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2F8CCDAB27FC6EE9004B7861 /* microsoft2020 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2F8CCDAE27FC6FF7004B7861 /* Microsoft2020UnprotectedBackup.swift */,
|
||||
2F8CCDB227FC7369004B7861 /* WalletMetadata.swift */,
|
||||
2F357882280983A900997F31 /* VcMetadata.swift */,
|
||||
2F8CCDB427FC745E004B7861 /* RawIdentity.swift */,
|
||||
2FD480102817CE98002AF37F /* Microsoft2020IdentifierBackup.swift */,
|
||||
);
|
||||
path = microsoft2020;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
55011E8A2572DF1C002D2690 /* logging */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -411,6 +483,7 @@
|
|||
55575730251BC575009979AB /* VCEntities */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2F53AA0A27E0CDC000C7C81E /* backup */,
|
||||
55B4D2D42787BB600086A9F1 /* oidc */,
|
||||
5517D28A25B90D0A00FBD239 /* exchange */,
|
||||
5573FEFD25B8DE6900282BC7 /* discovery */,
|
||||
|
@ -830,6 +903,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
55077ABA25A62DE10052C58D /* IdentifierFormatter.swift in Sources */,
|
||||
2F58B92F27FDA78F001C6DFE /* JwePasswordProtectionMethod.swift in Sources */,
|
||||
5518CC7625264D5700C7A21B /* ResponseMappings.swift in Sources */,
|
||||
5573FEF525B8DE4A00282BC7 /* IONDocumentModel.swift in Sources */,
|
||||
551F3068252E97000081D5E7 /* IdentifierCreator.swift in Sources */,
|
||||
|
@ -837,6 +911,7 @@
|
|||
55CA3A8127FB635300615CB6 /* VCSDKConfiguration.swift in Sources */,
|
||||
5599D0CC26A07EE10037C131 /* IssuancePin.swift in Sources */,
|
||||
559FC9AA25C9ABB500737F14 /* DomainLinkageCredentialValidator.swift in Sources */,
|
||||
2F53AA1027E0CF9700C7C81E /* UnprotectedBackupData.swift in Sources */,
|
||||
557BFC53251E665D005B5B24 /* IssuanceResponseFormatter.swift in Sources */,
|
||||
5584E4A02525656500A9DE58 /* InputDescriptorSchema.swift in Sources */,
|
||||
559FC9A625C8E42B00737F14 /* WellKnownConfigDocument.swift in Sources */,
|
||||
|
@ -856,6 +931,7 @@
|
|||
5573FF0025B8DE6900282BC7 /* IdentifierDocument.swift in Sources */,
|
||||
5580588725BFB6C00048E6FA /* PinDescriptor.swift in Sources */,
|
||||
5557576E251BC6CF009979AB /* ClaimDescriptor.swift in Sources */,
|
||||
2F8CCDAF27FC6FF7004B7861 /* Microsoft2020UnprotectedBackup.swift in Sources */,
|
||||
5557576B251BC6CF009979AB /* ContractInputDescriptor.swift in Sources */,
|
||||
5573FEFA25B8DE5900282BC7 /* IONDocumentInitialState.swift in Sources */,
|
||||
55575764251BC6CF009979AB /* PresentationDescriptor.swift in Sources */,
|
||||
|
@ -864,6 +940,7 @@
|
|||
55575767251BC6CF009979AB /* DisplayDescriptor.swift in Sources */,
|
||||
55575769251BC6CF009979AB /* IssuerDescriptor.swift in Sources */,
|
||||
55575778251BC6CF009979AB /* JSONCodingKeys.swift in Sources */,
|
||||
2F53AA0E27E0CEC800C7C81E /* ProtectedBackupData.swift in Sources */,
|
||||
5518CC7425264CAD00C7A21B /* PresentationResponseContainer.swift in Sources */,
|
||||
5518CC7225264C6F00C7A21B /* PresentationResponseFormatter.swift in Sources */,
|
||||
55AF7488252F6B53006A8B25 /* IdentifierDocumentSuffixDescriptor.swift in Sources */,
|
||||
|
@ -875,6 +952,7 @@
|
|||
5522D04F27DA727400617D15 /* AccessTokenDescriptor.swift in Sources */,
|
||||
55575763251BC6CF009979AB /* AttestationsDescriptor.swift in Sources */,
|
||||
559FCA5225CA042400737F14 /* LinkedDomainResult.swift in Sources */,
|
||||
2FB0154D2805C75400E67168 /* JwePasswordProtectedBackupData.swift in Sources */,
|
||||
55077ACA25A7763B0052C58D /* IdentifierDocumentPublicKey.swift in Sources */,
|
||||
5573FF0125B8DE6900282BC7 /* DiscoveryServiceResponse.swift in Sources */,
|
||||
559FCA3C25CA03D200737F14 /* IssuanceRequest.swift in Sources */,
|
||||
|
@ -887,6 +965,7 @@
|
|||
5573FEF625B8DE4A00282BC7 /* IONDocumentDeltaDescriptor.swift in Sources */,
|
||||
5557576C251BC6CF009979AB /* CardDisplayDescriptor.swift in Sources */,
|
||||
55AF748E252F6BE0006A8B25 /* Identifier.swift in Sources */,
|
||||
2F8CCDBD27FC88D3004B7861 /* UnprotectedBackup.swift in Sources */,
|
||||
55AF7486252F6B1F006A8B25 /* IdentifierDocumentServiceEndpoint.swift in Sources */,
|
||||
55BEF45D2589544500C720BD /* PinClaims.swift in Sources */,
|
||||
55A2CA6D266EA44B00AE1A20 /* MeasureTime.swift in Sources */,
|
||||
|
@ -898,6 +977,7 @@
|
|||
551F30432527DC050081D5E7 /* JwsHeaderFormatter .swift in Sources */,
|
||||
55575765251BC6CF009979AB /* ConsentDisplayDescriptor.swift in Sources */,
|
||||
5584E49C2525641600A9DE58 /* PresentationDefinition.swift in Sources */,
|
||||
2F357883280983A900997F31 /* VcMetadata.swift in Sources */,
|
||||
555CE08D2526810100C1C938 /* VerifiablePresentationClaims.swift in Sources */,
|
||||
55793BCD255A070E007F7599 /* VCEntitiesConstants.swift in Sources */,
|
||||
5584E4A2252565D900A9DE58 /* IssuanceMetadata.swift in Sources */,
|
||||
|
@ -907,8 +987,10 @@
|
|||
5517D28E25B90D0A00FBD239 /* ExchangeRequestClaims.swift in Sources */,
|
||||
559FC99025C8BEEB00737F14 /* DomainLinkageCredential.swift in Sources */,
|
||||
55379AD725A8DF7A0048600A /* PresentationRequestValidator.swift in Sources */,
|
||||
2F8CCDB327FC7369004B7861 /* WalletMetadata.swift in Sources */,
|
||||
55AC02B525D3087D00AA15F4 /* IdentifierDocumentServiceEndpointDescriptor.swift in Sources */,
|
||||
55011E8E2572DFBD002D2690 /* DefaultLogConsumer.swift in Sources */,
|
||||
2F53AA1227E0E74500C7C81E /* BackupProtectionMethod.swift in Sources */,
|
||||
5557576D251BC6CF009979AB /* SelfIssuedClaimsDescriptor.swift in Sources */,
|
||||
559FC96025C8940100737F14 /* DomainLinkageCredentialSubject.swift in Sources */,
|
||||
55BEF461258A5E0400C720BD /* IssuerIdToken.swift in Sources */,
|
||||
|
@ -917,6 +999,7 @@
|
|||
55FE6ED82697A06D00201EDC /* IssuanceCompletionResponse.swift in Sources */,
|
||||
55B4D2E02787BE5F0086A9F1 /* PresentationExchangeConstraints.swift in Sources */,
|
||||
559FC9B825C9C7DD00737F14 /* ContractServiceResponse.swift in Sources */,
|
||||
2F8CCDB527FC745E004B7861 /* RawIdentity.swift in Sources */,
|
||||
5584E49E252564A600A9DE58 /* PresentationInputDescriptor.swift in Sources */,
|
||||
5570597925754CA7000DD244 /* VCLogConsumer.swift in Sources */,
|
||||
5531D3AC255F03EF0002CC0E /* AliasComputer.swift in Sources */,
|
||||
|
@ -926,6 +1009,7 @@
|
|||
55B4D2DA2787BC7D0086A9F1 /* RequestedClaims.swift in Sources */,
|
||||
55B4D2D82787BC220086A9F1 /* SupportedVerifiablePresentationFormats.swift in Sources */,
|
||||
559FC98C25C8BEC900737F14 /* DomainLinkageCredentialContent.swift in Sources */,
|
||||
2FD480112817CE99002AF37F /* Microsoft2020IdentifierBackup.swift in Sources */,
|
||||
55575775251BC6CF009979AB /* IssuanceResponseClaims.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol UnprotectedBackup : Codable { }
|
||||
|
||||
/*
|
||||
public typealias CredentialBackup = (credential: VerifiableCredential, metadata: VcMetadata)
|
||||
|
||||
public struct IdentifierBackup {
|
||||
|
||||
public var master: Identifier?
|
||||
public var etc: [Identifier] = []
|
||||
|
||||
public init(withMasterIdentifer identifier:Identifier? = nil) {
|
||||
self.master = identifier;
|
||||
}
|
||||
|
||||
public mutating func addNonMaster(_ identifier:Identifier) {
|
||||
|
||||
if let master = self.master,
|
||||
master.alias == identifier.alias {
|
||||
return
|
||||
}
|
||||
etc.append(identifier)
|
||||
}
|
||||
|
||||
public var all : [Identifier] {
|
||||
|
||||
if let master = self.master {
|
||||
return [master] + self.etc
|
||||
}
|
||||
return self.etc
|
||||
}
|
||||
}
|
||||
|
||||
public struct UnprotectedBackup {
|
||||
public var seed: Data
|
||||
public var credentials: [CredentialBackup]
|
||||
public var identifiers: IdentifierBackup
|
||||
|
||||
public init(seed: Data,
|
||||
credentials: [CredentialBackup],
|
||||
identifiers: IdentifierBackup) {
|
||||
self.seed = seed
|
||||
self.credentials = credentials
|
||||
self.identifiers = identifiers
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
public protocol BackupProtectionMethod {
|
||||
func wrap(unprotectedBackupData: UnprotectedBackupData) throws -> ProtectedBackupData
|
||||
func unwrap(protectedBackupData: ProtectedBackupData) throws -> UnprotectedBackupData
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Foundation
|
||||
|
||||
struct JwePasswordProtectedBackupData : ProtectedBackupData {
|
||||
let content: String
|
||||
|
||||
public func serialize() -> String {
|
||||
content
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Foundation
|
||||
import VCCrypto
|
||||
import VCToken
|
||||
|
||||
enum JwePasswordProtectionError : Error {
|
||||
case invalidContentType
|
||||
}
|
||||
|
||||
public struct JwePasswordProtectionMethod : BackupProtectionMethod {
|
||||
|
||||
private struct Constants {
|
||||
static let Algorithm = "PBES2-HS512+A256KW"
|
||||
static let Method = "A256CBC-HS512"
|
||||
static let SaltLength = 8
|
||||
static let SaltIterations = UInt(100 * 1000)
|
||||
}
|
||||
|
||||
let password: String
|
||||
|
||||
public init(password:String) {
|
||||
self.password = password
|
||||
}
|
||||
|
||||
public func wrap(unprotectedBackupData: UnprotectedBackupData) throws -> ProtectedBackupData {
|
||||
|
||||
// Generate a salt
|
||||
let salt = try EphemeralSecret(size: Constants.SaltLength)
|
||||
|
||||
// Construct the headers
|
||||
let headers = Header(type: nil,
|
||||
algorithm: Constants.Algorithm,
|
||||
encryptionMethod: Constants.Method,
|
||||
jsonWebKey: nil,
|
||||
keyId: nil,
|
||||
contentType: unprotectedBackupData.type,
|
||||
pbes2SaltInput: salt.value.base64URLEncodedString(),
|
||||
pbes2Count: Constants.SaltIterations)
|
||||
|
||||
// Encrypt
|
||||
let token = try PbesJwe().encrypt(unprotectedBackupData.encoded,
|
||||
with: self.password,
|
||||
using: headers)
|
||||
|
||||
// Encode and return
|
||||
return try JwePasswordProtectedBackupData(content: JweEncoder().encode(token))
|
||||
}
|
||||
|
||||
public func unwrap(protectedBackupData: ProtectedBackupData) throws -> UnprotectedBackupData {
|
||||
|
||||
// Decode
|
||||
let token = try JweDecoder().decode(token: protectedBackupData.serialize())
|
||||
guard let type = token.headers.contentType else {
|
||||
throw JwePasswordProtectionError.invalidContentType
|
||||
}
|
||||
|
||||
// Decrypt
|
||||
let decrypted = try PbesJwe().decrypt(token, with: self.password)
|
||||
|
||||
// Return
|
||||
return UnprotectedBackupData(type: type, encoded: decrypted)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
public protocol ProtectedBackupData {
|
||||
func serialize() -> String
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Foundation
|
||||
|
||||
/// A ProtectedBackup holds a UnprotectedBackupData in some shape or form. The details are defined by implementations of this protocol. e.g. a JWE Token encrypted by a password.
|
||||
public struct UnprotectedBackupData {
|
||||
|
||||
public let type: String
|
||||
public let encoded: Data
|
||||
|
||||
public init(type: String, encoded: Data) {
|
||||
self.type = type
|
||||
self.encoded = encoded
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct Microsoft2020IdentifierBackup : Codable {
|
||||
|
||||
private struct Constants {
|
||||
static let OnTheWireMainIdentifier = "did.main.identifier"
|
||||
}
|
||||
|
||||
public var master: Identifier?
|
||||
public var etc: [Identifier]
|
||||
public var all: [Identifier] {
|
||||
if let master = self.master {
|
||||
return [master] + self.etc
|
||||
}
|
||||
return self.etc
|
||||
}
|
||||
|
||||
init() {
|
||||
self.master = nil
|
||||
self.etc = []
|
||||
}
|
||||
|
||||
public init(from decoder:Decoder) throws {
|
||||
|
||||
let container = try decoder.singleValueContainer()
|
||||
let decoded = try container.decode([RawIdentity].self)
|
||||
|
||||
var master: RawIdentity? = nil
|
||||
self.etc = []
|
||||
try decoded.forEach{ rawIdentity in
|
||||
if rawIdentity.name == Constants.OnTheWireMainIdentifier {
|
||||
master = rawIdentity
|
||||
return
|
||||
}
|
||||
try self.etc.append(rawIdentity.identifier)
|
||||
}
|
||||
master?.name = AliasComputer().compute(forId: VCEntitiesConstants.MASTER_ID,
|
||||
andRelyingParty: VCEntitiesConstants.MASTER_ID)
|
||||
self.master = try master?.identifier
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
|
||||
var output = try self.etc.map(RawIdentity.init)
|
||||
if let master = self.master {
|
||||
// For interoperability w/the Android implementation..
|
||||
var mapped = try RawIdentity(identifier: master)
|
||||
mapped.name = Constants.OnTheWireMainIdentifier
|
||||
output.insert(mapped, at: 0)
|
||||
}
|
||||
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode(output)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum Microsoft2020BackupError : Error {
|
||||
case undecodableCredential(source:String)
|
||||
}
|
||||
|
||||
open class Microsoft2020UnprotectedBackup : UnprotectedBackup {
|
||||
|
||||
private struct Constants {
|
||||
static let OnTheWireBackupType = "MicrosoftWallet2020"
|
||||
}
|
||||
|
||||
public var type = Constants.OnTheWireBackupType
|
||||
public var identifiers = Microsoft2020IdentifierBackup()
|
||||
public var vcs = [String:String]()
|
||||
public var vcsMetaInf = [String:VcMetadata]()
|
||||
|
||||
public var metaInf: WalletMetadata?
|
||||
|
||||
public init(metaInf: WalletMetadata? = nil) {
|
||||
self.metaInf = metaInf
|
||||
}
|
||||
|
||||
public func add(credential: VerifiableCredential, metadata:VcMetadata) {
|
||||
|
||||
if let id = credential.content.jti {
|
||||
self.vcs[id] = credential.rawValue
|
||||
self.vcsMetaInf[id] = metadata
|
||||
}
|
||||
}
|
||||
|
||||
public func forEachCredential(completionHandler: @escaping (VerifiableCredential, VcMetadata?) throws -> Void) rethrows {
|
||||
|
||||
try self.vcs.forEach { (key, value) in
|
||||
guard let credential = VerifiableCredential(from: value) else {
|
||||
// This might be better done during the parsing of the backup from JSON..?
|
||||
throw Microsoft2020BackupError.undecodableCredential(source: value)
|
||||
}
|
||||
|
||||
let metadata = self.vcsMetaInf[key]
|
||||
try completionHandler(credential, metadata)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Foundation
|
||||
import VCToken
|
||||
import VCCrypto
|
||||
|
||||
enum RawIdentityError: Error {
|
||||
case signingKeyNotFound
|
||||
case recoveryKeyNotFound
|
||||
case updateKeyNotFound
|
||||
case privateKeyNotFound
|
||||
case keyIdNotFound
|
||||
}
|
||||
|
||||
struct RawIdentity: Codable {
|
||||
var id: String
|
||||
var name: String
|
||||
var keys: [Jwk]?
|
||||
var recoveryKey: String
|
||||
var updateKey: String
|
||||
|
||||
init(identifier: Identifier) throws {
|
||||
|
||||
var keys = try identifier.didDocumentKeys.map(RawIdentity.jwkFromKeyContainer)
|
||||
try keys.append(RawIdentity.jwkFromKeyContainer(identifier.recoveryKey))
|
||||
try keys.append(RawIdentity.jwkFromKeyContainer(identifier.updateKey))
|
||||
self.id = identifier.did
|
||||
self.name = identifier.alias
|
||||
self.recoveryKey = identifier.recoveryKey.keyId
|
||||
self.updateKey = identifier.updateKey.keyId
|
||||
self.keys = keys
|
||||
}
|
||||
|
||||
var identifier: Identifier {
|
||||
get throws {
|
||||
// Get out the keys
|
||||
guard let keys = self.keys else {
|
||||
throw RawIdentityError.signingKeyNotFound
|
||||
}
|
||||
guard let recoveryJwk = keys.first(where: {$0.keyId == self.recoveryKey}) else {
|
||||
throw RawIdentityError.recoveryKeyNotFound
|
||||
}
|
||||
guard let updateJwk = keys.first(where: {$0.keyId == self.updateKey}) else {
|
||||
throw RawIdentityError.updateKeyNotFound
|
||||
}
|
||||
let set = Set([self.recoveryKey, self.updateKey])
|
||||
guard let signingJwk = keys.first(where: {!set.contains($0.keyId!)}) else {
|
||||
throw RawIdentityError.signingKeyNotFound
|
||||
}
|
||||
|
||||
// Convert
|
||||
let recoveryKeyContainer = try RawIdentity.keyContainerFromJwk(recoveryJwk)
|
||||
let updateKeyContainer = try RawIdentity.keyContainerFromJwk(updateJwk)
|
||||
let signingKeyContainer = try RawIdentity.keyContainerFromJwk(signingJwk)
|
||||
|
||||
// Wrap up and return
|
||||
return Identifier(longFormDid: self.id,
|
||||
didDocumentKeys: [signingKeyContainer],
|
||||
updateKey: updateKeyContainer,
|
||||
recoveryKey: recoveryKeyContainer,
|
||||
alias: self.name)
|
||||
}
|
||||
}
|
||||
|
||||
private static func jwkFromKeyContainer(_ keyContainer: KeyContainer) throws -> Jwk {
|
||||
|
||||
// Get out the public and private components of the key (pair)
|
||||
let secret = keyContainer.keyReference
|
||||
let publicKey = try Secp256k1().createPublicKey(forSecret: secret)
|
||||
let privateKey = try EphemeralSecret(with: secret)
|
||||
|
||||
// Wrap them up in a JSON Web Key
|
||||
return Jwk(keyType: "EC",
|
||||
keyId: keyContainer.keyId,
|
||||
curve: "secp256k1",
|
||||
use: "sig",
|
||||
x: publicKey.x,
|
||||
y: publicKey.y,
|
||||
d: privateKey.value)
|
||||
}
|
||||
|
||||
private static func keyContainerFromJwk(_ jwk: Jwk) throws -> KeyContainer {
|
||||
|
||||
guard let privateKeyData = jwk.d else {
|
||||
throw RawIdentityError.privateKeyNotFound
|
||||
}
|
||||
let privateKey = EphemeralSecret(with: privateKeyData,
|
||||
accessGroup: VCSDKConfiguration.sharedInstance.accessGroupIdentifier)
|
||||
|
||||
guard let keyId = jwk.keyId else {
|
||||
throw RawIdentityError.keyIdNotFound
|
||||
}
|
||||
|
||||
// Wrap it all up
|
||||
return KeyContainer(keyReference: privateKey, keyId: keyId)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
open class VcMetadata : Codable {
|
||||
|
||||
public var displayContract: DisplayDescriptor?
|
||||
public var type: String
|
||||
|
||||
public init(as type:String) {
|
||||
self.type = type
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Foundation
|
||||
import VCToken
|
||||
|
||||
open class WalletMetadata: Codable {
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case seed
|
||||
}
|
||||
|
||||
public var seed: Data
|
||||
|
||||
public init(with seed:Data) {
|
||||
self.seed = seed
|
||||
}
|
||||
|
||||
public required init(from decoder:Decoder) throws {
|
||||
|
||||
let container = try decoder.container(keyedBy: Self.CodingKeys)
|
||||
let string = try container.decode(String.self, forKey: .seed)
|
||||
|
||||
// Now, parse the string as a JWK
|
||||
let jwk = try JSONDecoder().decode(Jwk.self, from: string.data(using: .utf8)!)
|
||||
self.seed = jwk.key!
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
|
||||
// Wrap the seed value in a JWK and encode to a string
|
||||
let jwk = Jwk(keyType: "oct", key: self.seed)
|
||||
let string = try String(data: JSONEncoder().encode(jwk), encoding: .utf8)!
|
||||
|
||||
var container = encoder.container(keyedBy: Self.CodingKeys)
|
||||
try container.encode(string, forKey: .seed)
|
||||
}
|
||||
}
|
|
@ -10,12 +10,12 @@ public struct KeyContainer {
|
|||
/// key reference to key in Secret Store
|
||||
let keyReference: VCCryptoSecret
|
||||
|
||||
/// keyId to specify key in Identifier Document (must be less than 20 chars long)
|
||||
let keyId: String
|
||||
|
||||
/// Always ES256K because we only support Secp256k1 keys
|
||||
let algorithm: String = "ES256K"
|
||||
|
||||
/// keyId to specify key in Identifier Document (must be less than 20 chars long)
|
||||
public let keyId: String
|
||||
|
||||
public init(keyReference: VCCryptoSecret,
|
||||
keyId: String) {
|
||||
self.keyReference = keyReference
|
||||
|
@ -34,4 +34,8 @@ public struct KeyContainer {
|
|||
public func migrateKey(fromAccessGroup currentAccessGroup: String?) throws {
|
||||
try keyReference.migrateKey(fromAccessGroup: currentAccessGroup)
|
||||
}
|
||||
|
||||
public func persist(in secretStore:SecretStoring) throws {
|
||||
try secretStore.save(secret: self.keyReference)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,4 +16,13 @@ public struct CardDisplayDescriptor: Codable, Equatable {
|
|||
case title, issuedBy, backgroundColor, textColor, logo
|
||||
case cardDescription = "description"
|
||||
}
|
||||
|
||||
public init(title: String, issuedBy: String, backgroundColor: String, textColor: String, logo: LogoDisplayDescriptor?, cardDescription: String) {
|
||||
self.title = title
|
||||
self.issuedBy = issuedBy
|
||||
self.backgroundColor = backgroundColor
|
||||
self.textColor = textColor
|
||||
self.logo = logo
|
||||
self.cardDescription = cardDescription
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,4 +8,8 @@ public struct ClaimDisplayDescriptor: Codable, Equatable {
|
|||
public let type: String
|
||||
public let label: String
|
||||
|
||||
public init(type:String, label:String) {
|
||||
self.type = type;
|
||||
self.label = label;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,4 +8,8 @@ public struct ConsentDisplayDescriptor: Codable, Equatable {
|
|||
public let title: String?
|
||||
public let instructions: String
|
||||
|
||||
public init(title: String?, instructions: String) {
|
||||
self.title = title
|
||||
self.instructions = instructions
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,4 +12,12 @@ public struct DisplayDescriptor: Codable, Equatable {
|
|||
public let consent: ConsentDisplayDescriptor
|
||||
public let claims: [String: ClaimDisplayDescriptor]
|
||||
|
||||
public init(id: String?, locale: String?, contract: String?, card: CardDisplayDescriptor, consent: ConsentDisplayDescriptor, claims: [String : ClaimDisplayDescriptor]) {
|
||||
self.id = id
|
||||
self.locale = locale
|
||||
self.contract = contract
|
||||
self.card = card
|
||||
self.consent = consent
|
||||
self.claims = claims
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,12 @@ public struct LogoDisplayDescriptor: Codable, Equatable {
|
|||
// Image can be already included in the logo display descriptor as a base64 encoded png
|
||||
public let image: String?
|
||||
|
||||
public init(uri: String?, logoDescription: String?, image: String?) {
|
||||
self.uri = uri
|
||||
self.logoDescription = logoDescription
|
||||
self.image = image
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case uri
|
||||
case logoDescription = "description"
|
||||
|
|
|
@ -12,8 +12,10 @@ struct MockCryptoOperations: CryptoOperating {
|
|||
|
||||
static var generateKeyCallCount = 0
|
||||
let cryptoOperations: CryptoOperating
|
||||
let secretStore: SecretStoring
|
||||
|
||||
init(secretStore: SecretStoring) {
|
||||
self.secretStore = secretStore
|
||||
self.cryptoOperations = CryptoOperations(secretStore: secretStore, sdkConfiguration: VCSDKConfiguration.sharedInstance)
|
||||
}
|
||||
|
||||
|
@ -25,4 +27,8 @@ struct MockCryptoOperations: CryptoOperating {
|
|||
func retrieveKeyFromStorage(withId id: UUID) -> VCCryptoSecret {
|
||||
return KeyId(id: id)
|
||||
}
|
||||
|
||||
func retrieveKeyIfStored(uuid: UUID) throws -> VCCryptoSecret? {
|
||||
return KeyId(id: uuid)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,4 +21,14 @@ internal class SecretStoreMock: SecretStoring {
|
|||
func deleteSecret(id: UUID, itemTypeCode: String, accessGroup: String?) throws {
|
||||
memoryStore.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
func save(secret: VCCryptoSecret) throws {
|
||||
let ephemeral = try EphemeralSecret(with: secret)
|
||||
memoryStore[secret.id] = ephemeral.value
|
||||
}
|
||||
|
||||
func delete(secret: VCCryptoSecret) throws {
|
||||
let ephemeral = try EphemeralSecret(with: secret)
|
||||
memoryStore.removeValue(forKey: secret.id)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,16 @@ public class IdentifierService {
|
|||
return try identifierDB.fetchMasterIdentifier()
|
||||
}
|
||||
|
||||
public func fetchAllIdentifiers() throws -> [Identifier] {
|
||||
return try identifierDB.fetchAllIdentifiers()
|
||||
}
|
||||
|
||||
public func replaceIdentifiers(with identifiers:[Identifier]) throws {
|
||||
|
||||
try identifierDB.removeAllIdentifiers()
|
||||
try identifiers.forEach(identifierDB.importIdentifier)
|
||||
}
|
||||
|
||||
func fetchIdentifier(withAlias alias: String) throws -> Identifier {
|
||||
return try identifierDB.fetchIdentifier(withAlias: alias)
|
||||
}
|
||||
|
|
|
@ -40,7 +40,10 @@ public class CoreDataManager {
|
|||
signingKeyId: UUID,
|
||||
recoveryKeyId: UUID,
|
||||
updateKeyId: UUID,
|
||||
alias: String) throws {
|
||||
alias: String,
|
||||
signingKeyAlias: String? = nil,
|
||||
recoveryKeyAlias: String? = nil,
|
||||
updateKeyAlias: String? = nil) throws {
|
||||
guard let persistentContainer = persistentContainer else {
|
||||
throw CoreDataManagerError.persistentStoreNotLoaded
|
||||
}
|
||||
|
@ -53,6 +56,9 @@ public class CoreDataManager {
|
|||
model.signingKeyId = signingKeyId
|
||||
model.updateKeyId = updateKeyId
|
||||
model.alias = alias
|
||||
model.signingKeyAlias = signingKeyAlias
|
||||
model.recoveryKeyAlias = recoveryKeyAlias
|
||||
model.updateKeyAlias = updateKeyAlias
|
||||
|
||||
try persistentContainer.viewContext.save()
|
||||
}
|
||||
|
@ -81,6 +87,10 @@ public class CoreDataManager {
|
|||
try persistentContainer.viewContext.save()
|
||||
}
|
||||
|
||||
public func deleteIdentifer(_ model:IdentifierModel) {
|
||||
persistentContainer?.viewContext.delete(model)
|
||||
}
|
||||
|
||||
private func loadPersistentContainer(sdkLog: VCSDKLog) {
|
||||
|
||||
let messageKitBundle = Bundle(for: Self.self)
|
||||
|
|
|
@ -42,7 +42,10 @@ struct IdentifierDatabase {
|
|||
signingKeyId: signingKey.getId(),
|
||||
recoveryKeyId: identifier.recoveryKey.getId(),
|
||||
updateKeyId: identifier.updateKey.getId(),
|
||||
alias: identifier.alias)
|
||||
alias: identifier.alias,
|
||||
signingKeyAlias: signingKey.keyId,
|
||||
recoveryKeyAlias: identifier.recoveryKey.keyId,
|
||||
updateKeyAlias: identifier.updateKey.keyId)
|
||||
}
|
||||
|
||||
func fetchMasterIdentifier() throws -> Identifier {
|
||||
|
@ -88,6 +91,40 @@ struct IdentifierDatabase {
|
|||
return try createIdentifier(fromIdentifierModel: model)
|
||||
}
|
||||
|
||||
func fetchAllIdentifiers() throws -> [Identifier] {
|
||||
return try coreDataManager.fetchIdentifiers().map {
|
||||
try createIdentifier(fromIdentifierModel:$0)
|
||||
}
|
||||
}
|
||||
|
||||
func removeAllIdentifiers() throws {
|
||||
|
||||
try coreDataManager.fetchIdentifiers().forEach { identifierModel in
|
||||
// Step 1: remove the keys corresponding to each identifier
|
||||
let keyIds = [identifierModel.signingKeyId, identifierModel.updateKeyId, identifierModel.recoveryKeyId]
|
||||
try keyIds.forEach{ keyId in
|
||||
if let uuid = keyId,
|
||||
let key = try cryptoOperations.retrieveKeyIfStored(uuid: uuid) {
|
||||
try cryptoOperations.secretStore.delete(secret: key)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: delete the identifier
|
||||
coreDataManager.deleteIdentifer(identifierModel)
|
||||
}
|
||||
}
|
||||
|
||||
func importIdentifier(identifier: Identifier) throws {
|
||||
|
||||
// Put the keys in the keychain
|
||||
let keyContainers = identifier.didDocumentKeys + [identifier.updateKey, identifier.recoveryKey]
|
||||
try keyContainers.forEach { keyContainer in
|
||||
try keyContainer.persist(in: cryptoOperations.secretStore)
|
||||
}
|
||||
|
||||
try self.saveIdentifier(identifier: identifier)
|
||||
}
|
||||
|
||||
private func createIdentifier(fromIdentifierModel model: IdentifierModel) throws -> Identifier {
|
||||
|
||||
guard let longFormDid = model.did,
|
||||
|
@ -95,9 +132,12 @@ struct IdentifierDatabase {
|
|||
throw IdentifierDatabaseError.noAliasSavedInIdentifierModel
|
||||
}
|
||||
|
||||
let signingKeyContainer = try createKeyContainer(keyRefId: model.signingKeyId, keyId: VCEntitiesConstants.SIGNING_KEYID_PREFIX + alias)
|
||||
let updateKeyContainer = try createKeyContainer(keyRefId: model.updateKeyId, keyId: VCEntitiesConstants.UPDATE_KEYID_PREFIX + alias)
|
||||
let recoveryKeyContainer = try createKeyContainer(keyRefId: model.recoveryKeyId, keyId: VCEntitiesConstants.RECOVER_KEYID_PREFIX + alias)
|
||||
let signingKeyAlias = model.signingKeyAlias ?? VCEntitiesConstants.SIGNING_KEYID_PREFIX + alias
|
||||
let signingKeyContainer = try createKeyContainer(keyRefId: model.signingKeyId, keyId: signingKeyAlias)
|
||||
let updateKeyAlias = model.updateKeyAlias ?? VCEntitiesConstants.UPDATE_KEYID_PREFIX + alias
|
||||
let updateKeyContainer = try createKeyContainer(keyRefId: model.updateKeyId, keyId: updateKeyAlias)
|
||||
let recoveryKeyAlias = model.recoveryKeyAlias ?? VCEntitiesConstants.RECOVER_KEYID_PREFIX + alias
|
||||
let recoveryKeyContainer = try createKeyContainer(keyRefId: model.recoveryKeyId, keyId: recoveryKeyAlias)
|
||||
|
||||
return Identifier(longFormDid: longFormDid,
|
||||
didDocumentKeys: [signingKeyContainer],
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20D91" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="20086" systemVersion="21E258" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="IdentifierModel" representedClassName="IdentifierModel" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="alias" optional="YES" attributeType="String"/>
|
||||
<attribute name="did" optional="YES" attributeType="String"/>
|
||||
<attribute name="recoveryKeyAlias" optional="YES" attributeType="String"/>
|
||||
<attribute name="recoveryKeyId" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="signingKeyAlias" optional="YES" attributeType="String"/>
|
||||
<attribute name="signingKeyId" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="updateKeyAlias" optional="YES" attributeType="String"/>
|
||||
<attribute name="updateKeyId" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="IdentifierModel" positionX="-63" positionY="-18" width="128" height="104"/>
|
||||
<element name="IdentifierModel" positionX="-63" positionY="-18" width="128" height="149"/>
|
||||
</elements>
|
||||
</model>
|
|
@ -7,6 +7,11 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
2F29BA412808759F0076CB02 /* Jwk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F29BA402808759F0076CB02 /* Jwk.swift */; };
|
||||
2F3C363027E0EA670067F3BD /* JweToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F3C362F27E0EA670067F3BD /* JweToken.swift */; };
|
||||
2F58B93127FDBF13001C6DFE /* PbesJwe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F58B93027FDBF13001C6DFE /* PbesJwe.swift */; };
|
||||
2FA0117627F5BCC1006D5BF7 /* JweDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA0117527F5BCC1006D5BF7 /* JweDecoder.swift */; };
|
||||
2FBC5E3627F1BBD3000DB57F /* JweEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FBC5E3527F1BBD3000DB57F /* JweEncoder.swift */; };
|
||||
550F1E56251019BA009AF467 /* KeyId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 550F1E55251019BA009AF467 /* KeyId.swift */; };
|
||||
550F1E6125115866009AF467 /* ECPublicJwkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 550F1E6025115866009AF467 /* ECPublicJwkTests.swift */; };
|
||||
55269351250006020051A358 /* VCTokenError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55269350250006020051A358 /* VCTokenError.swift */; };
|
||||
|
@ -45,6 +50,11 @@
|
|||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
2F29BA402808759F0076CB02 /* Jwk.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Jwk.swift; sourceTree = "<group>"; };
|
||||
2F3C362F27E0EA670067F3BD /* JweToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JweToken.swift; sourceTree = "<group>"; };
|
||||
2F58B93027FDBF13001C6DFE /* PbesJwe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PbesJwe.swift; sourceTree = "<group>"; };
|
||||
2FA0117527F5BCC1006D5BF7 /* JweDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JweDecoder.swift; sourceTree = "<group>"; };
|
||||
2FBC5E3527F1BBD3000DB57F /* JweEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JweEncoder.swift; sourceTree = "<group>"; };
|
||||
550F1E55251019BA009AF467 /* KeyId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyId.swift; sourceTree = "<group>"; };
|
||||
550F1E6025115866009AF467 /* ECPublicJwkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ECPublicJwkTests.swift; sourceTree = "<group>"; };
|
||||
55269350250006020051A358 /* VCTokenError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCTokenError.swift; sourceTree = "<group>"; };
|
||||
|
@ -101,6 +111,7 @@
|
|||
55ADE27A24F5410300D9990E /* Secp256k1Signer.swift */,
|
||||
55C6C4A5250816500082BE73 /* Secp256k1Verifier.swift */,
|
||||
5540904A2500311E001246DB /* ECPublicJwk.swift */,
|
||||
2F58B93027FDBF13001C6DFE /* PbesJwe.swift */,
|
||||
);
|
||||
path = algorithms;
|
||||
sourceTree = "<group>";
|
||||
|
@ -128,6 +139,10 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
5526934F24FF32850051A358 /* algorithms */,
|
||||
2F29BA402808759F0076CB02 /* Jwk.swift */,
|
||||
2FBC5E3527F1BBD3000DB57F /* JweEncoder.swift */,
|
||||
2FA0117527F5BCC1006D5BF7 /* JweDecoder.swift */,
|
||||
2F3C362F27E0EA670067F3BD /* JweToken.swift */,
|
||||
55ADE27824F540F700D9990E /* JwsToken.swift */,
|
||||
55ADE27C24F5410E00D9990E /* Claims.swift */,
|
||||
55ADE27124F5407D00D9990E /* Header.swift */,
|
||||
|
@ -288,8 +303,13 @@
|
|||
files = (
|
||||
55ADE27D24F5410E00D9990E /* Claims.swift in Sources */,
|
||||
5540904B2500311E001246DB /* ECPublicJwk.swift in Sources */,
|
||||
2F3C363027E0EA670067F3BD /* JweToken.swift in Sources */,
|
||||
55C6C4A6250816500082BE73 /* Secp256k1Verifier.swift in Sources */,
|
||||
2F29BA412808759F0076CB02 /* Jwk.swift in Sources */,
|
||||
2FA0117627F5BCC1006D5BF7 /* JweDecoder.swift in Sources */,
|
||||
55ADE27924F540F700D9990E /* JwsToken.swift in Sources */,
|
||||
2F58B93127FDBF13001C6DFE /* PbesJwe.swift in Sources */,
|
||||
2FBC5E3627F1BBD3000DB57F /* JweEncoder.swift in Sources */,
|
||||
55ADE27224F5407D00D9990E /* Header.swift in Sources */,
|
||||
55269351250006020051A358 /* VCTokenError.swift in Sources */,
|
||||
55ADE28524F5412900D9990E /* TokenVerifying.swift in Sources */,
|
||||
|
|
|
@ -6,17 +6,29 @@
|
|||
public struct Header: Codable {
|
||||
public let type: String?
|
||||
public let algorithm: String?
|
||||
public let encryptionMethod: String?
|
||||
public let jsonWebKey: String?
|
||||
public let keyId: String?
|
||||
|
||||
public let contentType: String?
|
||||
public let pbes2SaltInput: String?
|
||||
public let pbes2Count: UInt?
|
||||
|
||||
public init(type: String? = nil,
|
||||
algorithm: String? = nil,
|
||||
encryptionMethod: String? = nil,
|
||||
jsonWebKey: String? = nil,
|
||||
keyId: String? = nil) {
|
||||
keyId: String? = nil,
|
||||
contentType: String? = nil,
|
||||
pbes2SaltInput: String? = nil,
|
||||
pbes2Count: UInt? = nil) {
|
||||
self.type = type
|
||||
self.algorithm = algorithm
|
||||
self.encryptionMethod = encryptionMethod
|
||||
self.jsonWebKey = jsonWebKey
|
||||
self.keyId = keyId
|
||||
self.contentType = contentType
|
||||
self.pbes2SaltInput = pbes2SaltInput
|
||||
self.pbes2Count = pbes2Count
|
||||
}
|
||||
|
||||
public enum CodingKeys: String, CodingKey {
|
||||
|
@ -24,6 +36,10 @@ public struct Header: Codable {
|
|||
case algorithm = "alg"
|
||||
case jsonWebKey = "jwk"
|
||||
case keyId = "kid"
|
||||
case contentType = "cty"
|
||||
case encryptionMethod = "enc"
|
||||
case pbes2SaltInput = "p2s"
|
||||
case pbes2Count = "p2c"
|
||||
}
|
||||
|
||||
// Note: implementing decode and encode to work around a compiler issue causing a EXC_BAD_ACCESS.
|
||||
|
@ -32,15 +48,23 @@ public struct Header: Codable {
|
|||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
type = try values.decodeIfPresent(String.self, forKey: .type)
|
||||
algorithm = try values.decodeIfPresent(String.self, forKey: .algorithm)
|
||||
encryptionMethod = try values.decodeIfPresent(String.self, forKey: .encryptionMethod)
|
||||
jsonWebKey = try values.decodeIfPresent(String.self, forKey: .jsonWebKey)
|
||||
keyId = try values.decodeIfPresent(String.self, forKey: .keyId)
|
||||
contentType = try values.decodeIfPresent(String.self, forKey: .contentType)
|
||||
pbes2SaltInput = try values.decodeIfPresent(String.self, forKey: .pbes2SaltInput)
|
||||
pbes2Count = try values.decodeIfPresent(UInt.self, forKey: .pbes2Count)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encodeIfPresent(type, forKey: .type)
|
||||
try container.encodeIfPresent(algorithm, forKey: .algorithm)
|
||||
try container.encodeIfPresent(encryptionMethod, forKey: .encryptionMethod)
|
||||
try container.encodeIfPresent(jsonWebKey, forKey: .jsonWebKey)
|
||||
try container.encodeIfPresent(keyId, forKey: .keyId)
|
||||
try container.encodeIfPresent(contentType, forKey: .contentType)
|
||||
try container.encodeIfPresent(pbes2SaltInput, forKey: .pbes2SaltInput)
|
||||
try container.encodeIfPresent(pbes2Count, forKey: .pbes2Count)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Foundation
|
||||
|
||||
enum JweDecoderError: Error {
|
||||
case unsupportedEncodingFormat
|
||||
case unableToInitializeJweToken
|
||||
}
|
||||
|
||||
public class JweDecoder {
|
||||
|
||||
private let json = JSONDecoder()
|
||||
|
||||
public init() {}
|
||||
|
||||
public func decode(token: String) throws -> JweToken {
|
||||
|
||||
let components = token.components(separatedBy: ".")
|
||||
guard components.count == 5 else {
|
||||
throw JweDecoderError.unsupportedEncodingFormat
|
||||
}
|
||||
|
||||
guard let headerData = Data(base64URLEncoded: components[0]) else {
|
||||
throw VCTokenError.unableToParseData
|
||||
}
|
||||
let headers = try json.decode(Header.self, from: headerData)
|
||||
|
||||
guard let aad = components[0].data(using: .nonLossyASCII) else {
|
||||
throw VCTokenError.unableToParseData
|
||||
}
|
||||
|
||||
guard let encryptedCek = Data(base64URLEncoded: components[1]) else {
|
||||
throw VCTokenError.unableToParseData
|
||||
}
|
||||
guard let iv = Data(base64URLEncoded: components[2]) else {
|
||||
throw VCTokenError.unableToParseData
|
||||
}
|
||||
guard let cipherText = Data(base64URLEncoded: components[3]) else {
|
||||
throw VCTokenError.unableToParseData
|
||||
}
|
||||
guard let authenticationTag = Data(base64URLEncoded: components[4]) else {
|
||||
throw VCTokenError.unableToParseData
|
||||
}
|
||||
|
||||
// Wrap it all up
|
||||
return JweToken(headers: headers, aad: aad, encryptedKey: encryptedCek, iv: iv, ciperText: cipherText, authenticationTag: authenticationTag)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum JweFormat {
|
||||
case compact
|
||||
}
|
||||
|
||||
public class JweEncoder {
|
||||
|
||||
private let json = JSONEncoder()
|
||||
|
||||
public init() {}
|
||||
|
||||
public func encode(_ token: JweToken, format: JweFormat = JweFormat.compact) throws -> String {
|
||||
switch format {
|
||||
case .compact:
|
||||
return try encodeUsingCompactFormat(token: token)
|
||||
}
|
||||
}
|
||||
|
||||
private func encodeUsingCompactFormat(token: JweToken) throws -> String {
|
||||
|
||||
var encoded = try json.encode(token.headers).base64URLEncodedString()
|
||||
encoded.append(".")
|
||||
encoded.append(token.encryptedKey.base64URLEncodedString())
|
||||
encoded.append(".")
|
||||
encoded.append(token.iv.base64URLEncodedString())
|
||||
encoded.append(".")
|
||||
encoded.append(token.ciperText.base64URLEncodedString())
|
||||
encoded.append(".")
|
||||
encoded.append(token.authenticationTag.base64URLEncodedString())
|
||||
return encoded
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct JweToken {
|
||||
public let headers: Header
|
||||
public let aad: Data
|
||||
public let encryptedKey: Data
|
||||
public let iv: Data
|
||||
public let ciperText: Data
|
||||
public let authenticationTag: Data
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Foundation
|
||||
|
||||
enum JwkError: Error {
|
||||
case invalidKeyValue
|
||||
}
|
||||
|
||||
/// Runtime container for JSON Web Keys, in which key material is Base64URLEncoded
|
||||
public struct Jwk: Codable, Equatable {
|
||||
|
||||
public let keyType: String
|
||||
public let keyId: String?
|
||||
public let key: Data?
|
||||
public let curve: String?
|
||||
public let use: String?
|
||||
public let x: Data?
|
||||
public let y: Data?
|
||||
public let d: Data?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case keyType = "kty"
|
||||
case keyId = "kid"
|
||||
case key = "k"
|
||||
case curve = "crv"
|
||||
case use, x, y, d
|
||||
}
|
||||
|
||||
public init(keyType: String,
|
||||
keyId: String? = nil,
|
||||
key: Data? = nil,
|
||||
curve: String? = nil,
|
||||
use: String? = nil,
|
||||
x: Data? = nil,
|
||||
y: Data? = nil,
|
||||
d: Data? = nil) {
|
||||
self.keyType = keyType
|
||||
self.keyId = keyId
|
||||
self.key = key
|
||||
self.curve = curve
|
||||
self.use = use
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.d = d
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
func parseKeyIfPresent(_ key: KeyedDecodingContainer<Jwk.CodingKeys>.Key) throws -> Data? {
|
||||
if let base64 = try values.decodeIfPresent(String.self, forKey: key) {
|
||||
guard let data = Data(base64URLEncoded: base64) else {
|
||||
throw JwkError.invalidKeyValue
|
||||
}
|
||||
return data
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
keyType = try values.decode(String.self, forKey: .keyType)
|
||||
keyId = try values.decodeIfPresent(String.self, forKey: .keyId)
|
||||
key = try parseKeyIfPresent(.key)
|
||||
curve = try values.decodeIfPresent(String.self, forKey: .curve)
|
||||
use = try values.decodeIfPresent(String.self, forKey: .use)
|
||||
x = try parseKeyIfPresent(.x)
|
||||
y = try parseKeyIfPresent(.y)
|
||||
d = try parseKeyIfPresent(.d)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encodeIfPresent(self.keyType, forKey: .keyType)
|
||||
try container.encodeIfPresent(self.keyId, forKey: .keyId)
|
||||
try container.encodeIfPresent(self.key?.base64URLEncodedString(), forKey: .key)
|
||||
try container.encodeIfPresent(self.curve, forKey: .curve)
|
||||
try container.encodeIfPresent(self.use, forKey: .use)
|
||||
try container.encodeIfPresent(self.x?.base64URLEncodedString(), forKey: .x)
|
||||
try container.encodeIfPresent(self.y?.base64URLEncodedString(), forKey: .y)
|
||||
try container.encodeIfPresent(self.d?.base64URLEncodedString(), forKey: .d)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Foundation
|
||||
import VCCrypto
|
||||
|
||||
enum PbesJweError: Error {
|
||||
case encodingError
|
||||
case invalidSaltInput
|
||||
case invalidAlgorithm
|
||||
case invalidEncryptionMethod
|
||||
case unauthenticatable
|
||||
}
|
||||
|
||||
/// Implementation of password-based encryption scheme for JSON Web Encryption
|
||||
public struct PbesJwe {
|
||||
|
||||
private struct Constants {
|
||||
static let InitializationVectorByteSize = 16
|
||||
}
|
||||
|
||||
private let aes = Aes()
|
||||
private let pbkdf = Pbkdf()
|
||||
|
||||
public init() {}
|
||||
|
||||
public func encrypt(_ plainText: Data, with password: String, using headers: Header) throws -> JweToken {
|
||||
|
||||
// Generate a content-encryption key
|
||||
guard let method = headers.encryptionMethod else {
|
||||
throw PbesJweError.invalidEncryptionMethod
|
||||
}
|
||||
let (_, keySize) = try HmacSha2AesCbc.props(for: method)
|
||||
let cek = try EphemeralSecret(size: (keySize*2))
|
||||
let keys = (cek.prefix(keySize), cek.suffix(keySize))
|
||||
|
||||
// Generate the additional authentication data, and the initialization vector, respectively
|
||||
guard let aad = try JSONEncoder().encode(headers).base64URLEncodedString().data(using: .nonLossyASCII) else {
|
||||
throw PbesJweError.encodingError
|
||||
}
|
||||
let iv = try EphemeralSecret(size: Constants.InitializationVectorByteSize)
|
||||
|
||||
// Generate the cipher text and message authentication code
|
||||
guard let alg = headers.algorithm else {
|
||||
throw PbesJweError.invalidAlgorithm
|
||||
}
|
||||
let hmac = try HmacSha2AesCbc(methodName: method)
|
||||
let (cipherText, mac) = try hmac.encrypt(plainText: plainText, using: aad, iv: iv.value, with: keys)
|
||||
|
||||
// Derive the key-encrypting key from the password
|
||||
guard let p2c = headers.pbes2Count,
|
||||
let p2s = headers.pbes2SaltInput else {
|
||||
throw PbesJweError.invalidSaltInput
|
||||
}
|
||||
let kek = try pbkdf.derive(from: password, withSaltInput: Data(base64URLEncoded: p2s)!, forAlgorithm: alg, rounds: UInt32(p2c))
|
||||
|
||||
// Wrap the content-encryption key with the key-encryption key
|
||||
let encryptedKey = try aes.wrap(key: cek, with: kek)
|
||||
|
||||
// Pull it all together
|
||||
let token = JweToken(headers: headers, aad: aad, encryptedKey: encryptedKey, iv: iv.value, ciperText: cipherText, authenticationTag: mac)
|
||||
return token
|
||||
}
|
||||
|
||||
public func decrypt(_ token: JweToken, with password: String) throws -> Data {
|
||||
|
||||
// Derive the key-encrypting key from the password
|
||||
guard let alg = token.headers.algorithm else {
|
||||
throw PbesJweError.invalidAlgorithm
|
||||
}
|
||||
guard let p2c = token.headers.pbes2Count,
|
||||
let p2s = token.headers.pbes2SaltInput else {
|
||||
throw PbesJweError.invalidSaltInput
|
||||
}
|
||||
let kek = try pbkdf.derive(from: password, withSaltInput: Data(base64URLEncoded: p2s)!, forAlgorithm: alg, rounds: UInt32(p2c))
|
||||
|
||||
// Unwrap the content-encryption key
|
||||
guard let method = token.headers.encryptionMethod else {
|
||||
throw PbesJweError.invalidEncryptionMethod
|
||||
}
|
||||
let (_, keySize) = try HmacSha2AesCbc.props(for: method)
|
||||
let unwrapped = try aes.unwrap(wrapped: token.encryptedKey, using: kek)
|
||||
let cek = try EphemeralSecret(with: unwrapped)
|
||||
let keys = (cek.prefix(keySize), cek.suffix(keySize))
|
||||
|
||||
// Authenticate and decrypt the plaintext
|
||||
let hmac = try HmacSha2AesCbc(methodName: method)
|
||||
let plainText = try hmac.decrypt((token.ciperText, token.authenticationTag), using: token.aad, iv: token.iv, with: keys)
|
||||
return plainText
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче