Add support for password-encrypted, file-based import and export of verifiable credentials (#128)
Co-authored-by: Sydney Morton <symorton@microsoft.com>
This commit is contained in:
Родитель
c7ebcbbcb5
Коммит
fa1a27768d
|
@ -7,6 +7,13 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
2F848B5B282D50D5005D3176 /* Pbkdf.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F848B57282D50D5005D3176 /* Pbkdf.swift */; };
|
||||
2F848B5C282D50D5005D3176 /* Aes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F848B58282D50D5005D3176 /* Aes.swift */; };
|
||||
2F848B5D282D50D5005D3176 /* HmacSha2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F848B59282D50D5005D3176 /* HmacSha2.swift */; };
|
||||
2F848B5E282D50D5005D3176 /* HmacSha2AesCbc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F848B5A282D50D5005D3176 /* HmacSha2AesCbc.swift */; };
|
||||
2F848B60282D514F005D3176 /* EphemeralSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F848B5F282D514F005D3176 /* EphemeralSecret.swift */; };
|
||||
2F848B63282D51D7005D3176 /* AesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F848B61282D51D7005D3176 /* AesTests.swift */; };
|
||||
2F848B64282D51D7005D3176 /* PbkdfTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F848B62282D51D7005D3176 /* PbkdfTests.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 */
|
||||
2F848B57282D50D5005D3176 /* Pbkdf.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pbkdf.swift; sourceTree = "<group>"; };
|
||||
2F848B58282D50D5005D3176 /* Aes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Aes.swift; sourceTree = "<group>"; };
|
||||
2F848B59282D50D5005D3176 /* HmacSha2.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HmacSha2.swift; sourceTree = "<group>"; };
|
||||
2F848B5A282D50D5005D3176 /* HmacSha2AesCbc.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HmacSha2AesCbc.swift; sourceTree = "<group>"; };
|
||||
2F848B5F282D514F005D3176 /* EphemeralSecret.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EphemeralSecret.swift; sourceTree = "<group>"; };
|
||||
2F848B61282D51D7005D3176 /* AesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AesTests.swift; sourceTree = "<group>"; };
|
||||
2F848B62282D51D7005D3176 /* PbkdfTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PbkdfTests.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>"; };
|
||||
|
@ -131,6 +145,7 @@
|
|||
928AAF6F24EEDC670029A8F9 /* Keys */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2F848B5F282D514F005D3176 /* EphemeralSecret.swift */,
|
||||
92CDE80524E1ABEA00F95B5D /* Random32BytesSecret.swift */,
|
||||
92CDE80724E1F7AF00F95B5D /* Secret.swift */,
|
||||
928AAF7024EEDC9D0029A8F9 /* Secp256k1PublicKey.swift */,
|
||||
|
@ -186,6 +201,8 @@
|
|||
928AAF7224EEE0AB0029A8F9 /* Secp256k1PublicKeyTests.swift */,
|
||||
92CDE7E724DE1A2000F95B5D /* KeychainSecretStoreTests.swift */,
|
||||
92CDE80B24E213AE00F95B5D /* Random32BytesSecretTests.swift */,
|
||||
2F848B61282D51D7005D3176 /* AesTests.swift */,
|
||||
2F848B62282D51D7005D3176 /* PbkdfTests.swift */,
|
||||
);
|
||||
path = VCCryptoTests;
|
||||
sourceTree = "<group>";
|
||||
|
@ -202,6 +219,10 @@
|
|||
92CDE7D024DCA69C00F95B5D /* Algo */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2F848B58282D50D5005D3176 /* Aes.swift */,
|
||||
2F848B59282D50D5005D3176 /* HmacSha2.swift */,
|
||||
2F848B5A282D50D5005D3176 /* HmacSha2AesCbc.swift */,
|
||||
2F848B57282D50D5005D3176 /* Pbkdf.swift */,
|
||||
92CDE7D124DCA6CF00F95B5D /* Secp256k1.swift */,
|
||||
92CDE7D824DDBA0500F95B5D /* HmacSha512.swift */,
|
||||
92CDE7DA24DDC65100F95B5D /* Sha512.swift */,
|
||||
|
@ -409,13 +430,18 @@
|
|||
92CDE7CB24DCA61800F95B5D /* KeychainSecretStore.swift in Sources */,
|
||||
92CDE7DE24DDCB9500F95B5D /* Data+Hex.swift in Sources */,
|
||||
92CDE80624E1ABEA00F95B5D /* Random32BytesSecret.swift in Sources */,
|
||||
2F848B5B282D50D5005D3176 /* Pbkdf.swift in Sources */,
|
||||
92CDE7D924DDBA0500F95B5D /* HmacSha512.swift in Sources */,
|
||||
2F848B5D282D50D5005D3176 /* HmacSha2.swift in Sources */,
|
||||
553D4AAF280F443A00FD39A6 /* VCSDKConfigurable.swift in Sources */,
|
||||
55ADE29924F5752500D9990E /* Sha256.swift in Sources */,
|
||||
92CDE80824E1F7AF00F95B5D /* Secret.swift in Sources */,
|
||||
92CDE7D224DCA6CF00F95B5D /* Secp256k1.swift in Sources */,
|
||||
2F848B5C282D50D5005D3176 /* Aes.swift in Sources */,
|
||||
928AAF7124EEDC9D0029A8F9 /* Secp256k1PublicKey.swift in Sources */,
|
||||
5540903D25000F6A001246DB /* Signing.swift in Sources */,
|
||||
2F848B5E282D50D5005D3176 /* HmacSha2AesCbc.swift in Sources */,
|
||||
2F848B60282D514F005D3176 /* EphemeralSecret.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -426,11 +452,13 @@
|
|||
928AAF7324EEE0AB0029A8F9 /* Secp256k1PublicKeyTests.swift in Sources */,
|
||||
92CDE7D724DCD07D00F95B5D /* Secp256k1Tests.swift in Sources */,
|
||||
92CDE7D524DCCE3C00F95B5D /* SecretStoreMock.swift in Sources */,
|
||||
2F848B63282D51D7005D3176 /* AesTests.swift in Sources */,
|
||||
92CDE80A24E2047900F95B5D /* SecretMock.swift in Sources */,
|
||||
92CDE80C24E213AE00F95B5D /* Random32BytesSecretTests.swift in Sources */,
|
||||
92CDE7E224DDF1A900F95B5D /* HmacSha512Tests.swift in Sources */,
|
||||
92CDE7E424DDF42A00F95B5D /* Utf8TestData.swift in Sources */,
|
||||
92CDE7E024DDD24700F95B5D /* Sha512Tests.swift in Sources */,
|
||||
2F848B64282D51D7005D3176 /* PbkdfTests.swift in Sources */,
|
||||
55ADE29B24F5757100D9990E /* Sha256Tests.swift in Sources */,
|
||||
92CDE7E824DE1A2000F95B5D /* KeychainSecretStoreTests.swift in Sources */,
|
||||
);
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 cryptoError(operation:CCOperation, status:CCCryptorStatus)
|
||||
}
|
||||
|
||||
public struct Aes {
|
||||
|
||||
// AES processes inputs in blocks of 128 bits (16 bytes)
|
||||
internal let blockSize = size_t(16)
|
||||
|
||||
private let keyWrapAlg = CCWrappingAlgorithm(kCCWRAPAES)
|
||||
|
||||
public init() {}
|
||||
|
||||
public func wrap(key: VCCryptoSecret, with kek: VCCryptoSecret) throws -> Data {
|
||||
|
||||
// Look for an early out
|
||||
var wrappedSize: Int = 0
|
||||
try key.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.withUnsafeBytes { (keyPtr: UnsafeRawBufferPointer) in
|
||||
let keyBytes = keyPtr.bindMemory(to: UInt8.self)
|
||||
|
||||
try kek.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 {
|
||||
|
||||
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.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)
|
||||
}
|
||||
|
||||
private func apply(operation: CCOperation, withOptions options: CCOptions, to data: Data, using key: VCCryptoSecret, iv: Data) throws -> Data {
|
||||
|
||||
// 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.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,69 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 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 }
|
||||
|
||||
// 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.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,10 @@
|
|||
import Foundation
|
||||
import CommonCrypto
|
||||
|
||||
enum HmacSha512Error: Error {
|
||||
case invalidMessage
|
||||
case invalidSecret
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Superceded by HmacSha2")
|
||||
public struct HmacSha512 {
|
||||
|
||||
public init() { }
|
||||
|
||||
/// Authenticate a message
|
||||
/// - Parameters:
|
||||
|
@ -19,17 +17,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 +28,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,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)
|
||||
}
|
||||
}
|
|
@ -13,7 +13,6 @@ enum Secp256k1Error: Error {
|
|||
case invalidSignature
|
||||
case invalidPublicKey
|
||||
case publicKeyCreationFailure
|
||||
case invalidSecret
|
||||
}
|
||||
|
||||
public struct Secp256k1: Signing {
|
||||
|
@ -28,7 +27,6 @@ public struct Secp256k1: Signing {
|
|||
public func sign(messageHash: Data, withSecret secret: VCCryptoSecret) throws -> Data {
|
||||
|
||||
// Validate params
|
||||
guard secret is Secret else { throw Secp256k1Error.invalidSecret }
|
||||
guard messageHash.count == 32 else { throw Secp256k1Error.invalidMessageHash }
|
||||
|
||||
// Create the context and signature data structure
|
||||
|
@ -42,7 +40,7 @@ public struct Secp256k1: Signing {
|
|||
}
|
||||
|
||||
// Sign the message
|
||||
try (secret as! Secret).withUnsafeBytes { (secretPtr) in
|
||||
try secret.withUnsafeBytes { (secretPtr) in
|
||||
guard secp256k1_ec_seckey_verify(context!, secretPtr.bindMemory(to: UInt8.self).baseAddress.unsafelyUnwrapped) > 0 else { throw Secp256k1Error.invalidSecretKey }
|
||||
|
||||
try messageHash.withUnsafeBytes { (msgPtr) in
|
||||
|
@ -125,15 +123,35 @@ public struct Secp256k1: Signing {
|
|||
/// - Parameter secret: The Secret used to generate the public key
|
||||
/// - Returns: The public key
|
||||
public func createPublicKey(forSecret secret: VCCryptoSecret) throws -> Secp256k1PublicKey {
|
||||
// Validate params
|
||||
guard secret is Secret else { throw Secp256k1Error.invalidSecret }
|
||||
|
||||
let (_, publicKey) = try self.createKeyPair(forSecret: secret)
|
||||
return publicKey
|
||||
}
|
||||
|
||||
/// Create a key pair from a secret
|
||||
/// - Parameter secret: The Secret used to generate the public key
|
||||
/// - Returns: The key pair
|
||||
public func createKeyPair(forSecret secret: VCCryptoSecret) throws -> (EphemeralSecret, Secp256k1PublicKey)
|
||||
{
|
||||
// Get out the private key data
|
||||
let privateKey = try EphemeralSecret(with: secret)
|
||||
|
||||
// Return with the public key
|
||||
return (privateKey, try self.createPublicKey(forPrivateKey: privateKey))
|
||||
}
|
||||
|
||||
/// Create a public key from a private key
|
||||
/// - Parameter secret: The Secret used to generate the public key
|
||||
/// - Returns: The public key
|
||||
public func createPublicKey(forPrivateKey privateKey:EphemeralSecret) throws -> Secp256k1PublicKey {
|
||||
|
||||
// Create the context and public key data structure
|
||||
let context = secp256k1_context_create(UInt32(SECP256K1_CONTEXT_SIGN))!
|
||||
defer { secp256k1_context_destroy(context) }
|
||||
let pubkey = UnsafeMutablePointer<secp256k1_pubkey>.allocate(capacity: 1)
|
||||
|
||||
// Create the public key
|
||||
try (secret as! Secret).withUnsafeBytes { (secretPtr) in
|
||||
|
||||
// Populate it
|
||||
try privateKey.withUnsafeBytes { (secretPtr) in
|
||||
let result = secp256k1_ec_pubkey_create(
|
||||
context,
|
||||
pubkey,
|
||||
|
@ -156,6 +174,7 @@ public struct Secp256k1: Signing {
|
|||
return
|
||||
}
|
||||
|
||||
// Wrap it all up
|
||||
return Secp256k1PublicKey(uncompressedPublicKey: publicKey)!
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,9 @@
|
|||
public protocol CryptoOperating {
|
||||
func generateKey() throws -> VCCryptoSecret
|
||||
func retrieveKeyFromStorage(withId id: UUID) -> VCCryptoSecret
|
||||
func save(key: Data, withId id: UUID) throws
|
||||
func deleteKey(withId id: UUID) throws
|
||||
func getKey(withId id: UUID) throws -> Data
|
||||
}
|
||||
|
||||
public struct CryptoOperations: CryptoOperating {
|
||||
|
@ -33,4 +36,45 @@ public struct CryptoOperations: CryptoOperating {
|
|||
let accessGroup = sdkConfiguration.accessGroupIdentifier
|
||||
return Random32BytesSecret(withStore: secretStore, andId: id, inAccessGroup: accessGroup)
|
||||
}
|
||||
|
||||
public func save(key: Data, withId id: UUID) throws {
|
||||
|
||||
// Take a copy of the key to let the store dispose of it
|
||||
var data = Data()
|
||||
data.append(key)
|
||||
|
||||
// Format the item type code
|
||||
let itemTypeCode = String(format: "r%02dB", data.count)
|
||||
|
||||
// Store down
|
||||
try secretStore.saveSecret(id: id,
|
||||
itemTypeCode: itemTypeCode,
|
||||
accessGroup: sdkConfiguration.accessGroupIdentifier,
|
||||
value: &data)
|
||||
}
|
||||
|
||||
public func deleteKey(withId id: UUID) throws {
|
||||
|
||||
let itemTypeCode = Random32BytesSecret.itemTypeCode
|
||||
let accessGroup = sdkConfiguration.accessGroupIdentifier
|
||||
do {
|
||||
let _ = try secretStore.getSecret(id: id,
|
||||
itemTypeCode: itemTypeCode,
|
||||
accessGroup: accessGroup)
|
||||
|
||||
// If we get here the key exists, so we can delete it
|
||||
try secretStore.deleteSecret(id: id, itemTypeCode: itemTypeCode, accessGroup: accessGroup)
|
||||
}
|
||||
catch SecretStoringError.itemNotFound {
|
||||
/* There's no key so nothing to delete */
|
||||
}
|
||||
}
|
||||
|
||||
public func getKey(withId id: UUID) throws -> Data {
|
||||
|
||||
return try secretStore.getSecret(id: id,
|
||||
itemTypeCode: Random32BytesSecret.itemTypeCode,
|
||||
accessGroup: sdkConfiguration.accessGroupIdentifier)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,17 +5,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 {
|
||||
struct KeychainSecretStore: SecretStoring {
|
||||
|
||||
private let vcService = "com.microsoft.vcCrypto"
|
||||
|
||||
|
@ -48,9 +38,19 @@ struct KeychainSecretStore : SecretStoring {
|
|||
|
||||
var item: CFTypeRef?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
||||
guard status != errSecItemNotFound else { throw KeychainStoreError.itemNotFound }
|
||||
guard status == errSecSuccess else { throw KeychainStoreError.readFromStoreError(status: status as OSStatus) }
|
||||
guard var value = item as? Data else { throw KeychainStoreError.invalidItemInStore }
|
||||
|
||||
var statusMessage: String? = nil
|
||||
if #available(iOS 11.3, *) {
|
||||
statusMessage = SecCopyErrorMessageString(status, nil) as? String
|
||||
}
|
||||
|
||||
guard status != errSecItemNotFound else { throw SecretStoringError.itemNotFound }
|
||||
|
||||
guard status == errSecSuccess else {
|
||||
throw SecretStoringError.readFromStoreError(status: status as OSStatus, message: statusMessage)
|
||||
}
|
||||
|
||||
guard var value = item as? Data else { throw SecretStoringError.invalidItemInStore }
|
||||
defer {
|
||||
let secretSize = value.count
|
||||
value.withUnsafeMutableBytes { (secretPtr) in
|
||||
|
@ -78,7 +78,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
|
||||
|
@ -103,8 +103,14 @@ struct KeychainSecretStore : SecretStoring {
|
|||
}
|
||||
|
||||
let status = SecItemAdd(query as CFDictionary, nil)
|
||||
|
||||
var statusMessage: String? = nil
|
||||
if #available(iOS 11.3, *) {
|
||||
statusMessage = SecCopyErrorMessageString(status, nil) as? String
|
||||
}
|
||||
|
||||
guard status == errSecSuccess else {
|
||||
throw KeychainStoreError.saveToStoreError(status: status)
|
||||
throw SecretStoringError.saveToStoreError(status: status, message: statusMessage)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -116,7 +122,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 +139,14 @@ struct KeychainSecretStore : SecretStoring {
|
|||
}
|
||||
|
||||
let status = SecItemDelete(query as CFDictionary)
|
||||
guard status == errSecSuccess else {
|
||||
throw KeychainStoreError.deleteFromStoreError(status: status)
|
||||
|
||||
var statusMessage: String? = nil
|
||||
if #available(iOS 11.3, *) {
|
||||
statusMessage = SecCopyErrorMessageString(status, nil) as? String
|
||||
}
|
||||
|
||||
guard status == errSecSuccess || status == errSecItemNotFound else {
|
||||
throw SecretStoringError.deleteFromStoreError(status: status, message: statusMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* 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.withUnsafeBytes { secretPtr in
|
||||
if let baseAddress = secretPtr.baseAddress, secretPtr.count > 0 {
|
||||
let bytes = baseAddress.assumingMemoryBound(to: UInt8.self)
|
||||
value.append(bytes, count: secretPtr.count)
|
||||
}
|
||||
}
|
||||
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() throws {}
|
||||
|
||||
public func migrateKey(fromAccessGroup currentAccessGroup: String?) throws {
|
||||
/* Do nothing */
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -62,13 +62,8 @@ final class Random32BytesSecret: Secret {
|
|||
}
|
||||
}
|
||||
|
||||
func isValidKey() -> Bool {
|
||||
do {
|
||||
_ = try self.store.getSecret(id: id, itemTypeCode: Random32BytesSecret.itemTypeCode, accessGroup: accessGroup)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
func isValidKey() throws {
|
||||
_ = try self.store.getSecret(id: id, itemTypeCode: Random32BytesSecret.itemTypeCode, accessGroup: accessGroup)
|
||||
}
|
||||
|
||||
func migrateKey(fromAccessGroup currentAccessGroup: String?) throws {
|
||||
|
|
|
@ -15,16 +15,16 @@ public protocol VCCryptoSecret {
|
|||
/// Access group for key.
|
||||
var accessGroup: String? { get }
|
||||
|
||||
func isValidKey() -> Bool
|
||||
func isValidKey() throws
|
||||
|
||||
func migrateKey(fromAccessGroup currentAccessGroup: String?) throws
|
||||
|
||||
/// Invokes the closure passed as a param with a buffer pointer to the raw bytes of the secret.
|
||||
func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> Void) throws
|
||||
}
|
||||
|
||||
protocol InternalSecret {
|
||||
|
||||
/// Invokes the closure passed as a param with a buffer pointer to the raw bytes of the secret.
|
||||
func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> Void) throws
|
||||
|
||||
/// The 4 characters representing the secret type in the store. This correspond to kSecAttrType in keychain
|
||||
static var itemTypeCode: String { get }
|
||||
}
|
||||
|
|
|
@ -5,10 +5,21 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public enum SecretStoringError: Error {
|
||||
case itemNotFound
|
||||
case invalidItemInStore
|
||||
case itemAlreadyInStore
|
||||
case invalidType
|
||||
case deleteFromStoreError(status: OSStatus, message: String? = nil)
|
||||
case saveToStoreError(status: Int32, message: String? = nil)
|
||||
case readFromStoreError(status: OSStatus, message: String? = nil)
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
}
|
||||
|
|
|
@ -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.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.
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,9 +28,7 @@ final class SecretMock : Secret {
|
|||
}
|
||||
}
|
||||
|
||||
func isValidKey() -> Bool {
|
||||
return true
|
||||
}
|
||||
func isValidKey() throws { }
|
||||
|
||||
func migrateKey(fromAccessGroup oldAccessGroup: String?) throws { }
|
||||
}
|
||||
|
|
|
@ -21,4 +21,5 @@ internal class SecretStoreMock: SecretStoring {
|
|||
func deleteSecret(id: UUID, itemTypeCode: String, accessGroup: String?) throws {
|
||||
memoryStore.removeValue(forKey: 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.withUnsafeBytes { (derivedPtr) in
|
||||
data.append(derivedPtr.bindMemory(to: UInt8.self))
|
||||
}
|
||||
XCTAssertEqual(Data(expected), data)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,17 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
2F848B7F282D5D25005D3176 /* UnprotectedBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F848B70282D5D25005D3176 /* UnprotectedBackup.swift */; };
|
||||
2F848B80282D5D25005D3176 /* ProtectedBackupData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F848B72282D5D25005D3176 /* ProtectedBackupData.swift */; };
|
||||
2F848B81282D5D25005D3176 /* Microsoft2020IdentifierBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F848B74282D5D25005D3176 /* Microsoft2020IdentifierBackup.swift */; };
|
||||
2F848B82282D5D25005D3176 /* RawIdentity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F848B75282D5D25005D3176 /* RawIdentity.swift */; };
|
||||
2F848B83282D5D25005D3176 /* VcMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F848B76282D5D25005D3176 /* VcMetadata.swift */; };
|
||||
2F848B84282D5D25005D3176 /* Microsoft2020UnprotectedBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F848B77282D5D25005D3176 /* Microsoft2020UnprotectedBackup.swift */; };
|
||||
2F848B85282D5D25005D3176 /* WalletMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F848B78282D5D25005D3176 /* WalletMetadata.swift */; };
|
||||
2F848B86282D5D25005D3176 /* UnprotectedBackupData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F848B79282D5D25005D3176 /* UnprotectedBackupData.swift */; };
|
||||
2F848B87282D5D25005D3176 /* BackupProtectionMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F848B7B282D5D25005D3176 /* BackupProtectionMethod.swift */; };
|
||||
2F848B88282D5D25005D3176 /* JwePasswordProtectedBackupData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F848B7D282D5D25005D3176 /* JwePasswordProtectedBackupData.swift */; };
|
||||
2F848B89282D5D25005D3176 /* JwePasswordProtectionMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F848B7E282D5D25005D3176 /* JwePasswordProtectionMethod.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 */; };
|
||||
|
@ -147,6 +158,18 @@
|
|||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
2F4EEF80282E758E00B62311 /* PromiseKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PromiseKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
2F848B70282D5D25005D3176 /* UnprotectedBackup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnprotectedBackup.swift; sourceTree = "<group>"; };
|
||||
2F848B72282D5D25005D3176 /* ProtectedBackupData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProtectedBackupData.swift; sourceTree = "<group>"; };
|
||||
2F848B74282D5D25005D3176 /* Microsoft2020IdentifierBackup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Microsoft2020IdentifierBackup.swift; sourceTree = "<group>"; };
|
||||
2F848B75282D5D25005D3176 /* RawIdentity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RawIdentity.swift; sourceTree = "<group>"; };
|
||||
2F848B76282D5D25005D3176 /* VcMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VcMetadata.swift; sourceTree = "<group>"; };
|
||||
2F848B77282D5D25005D3176 /* Microsoft2020UnprotectedBackup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Microsoft2020UnprotectedBackup.swift; sourceTree = "<group>"; };
|
||||
2F848B78282D5D25005D3176 /* WalletMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletMetadata.swift; sourceTree = "<group>"; };
|
||||
2F848B79282D5D25005D3176 /* UnprotectedBackupData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnprotectedBackupData.swift; sourceTree = "<group>"; };
|
||||
2F848B7B282D5D25005D3176 /* BackupProtectionMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupProtectionMethod.swift; sourceTree = "<group>"; };
|
||||
2F848B7D282D5D25005D3176 /* JwePasswordProtectedBackupData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JwePasswordProtectedBackupData.swift; sourceTree = "<group>"; };
|
||||
2F848B7E282D5D25005D3176 /* JwePasswordProtectionMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JwePasswordProtectionMethod.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>"; };
|
||||
|
@ -303,6 +326,56 @@
|
|||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
2F848B6F282D5D25005D3176 /* backup */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2F848B70282D5D25005D3176 /* UnprotectedBackup.swift */,
|
||||
2F848B71282D5D25005D3176 /* content */,
|
||||
2F848B7A282D5D25005D3176 /* container */,
|
||||
);
|
||||
path = backup;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2F848B71282D5D25005D3176 /* content */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2F848B73282D5D25005D3176 /* microsoft2020 */,
|
||||
2F848B72282D5D25005D3176 /* ProtectedBackupData.swift */,
|
||||
2F848B79282D5D25005D3176 /* UnprotectedBackupData.swift */,
|
||||
);
|
||||
path = content;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2F848B73282D5D25005D3176 /* microsoft2020 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2F848B74282D5D25005D3176 /* Microsoft2020IdentifierBackup.swift */,
|
||||
2F848B75282D5D25005D3176 /* RawIdentity.swift */,
|
||||
2F848B76282D5D25005D3176 /* VcMetadata.swift */,
|
||||
2F848B77282D5D25005D3176 /* Microsoft2020UnprotectedBackup.swift */,
|
||||
2F848B78282D5D25005D3176 /* WalletMetadata.swift */,
|
||||
);
|
||||
path = microsoft2020;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2F848B7A282D5D25005D3176 /* container */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2F848B7B282D5D25005D3176 /* BackupProtectionMethod.swift */,
|
||||
2F848B7C282D5D25005D3176 /* jwe */,
|
||||
);
|
||||
path = container;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2F848B7C282D5D25005D3176 /* jwe */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2F848B7D282D5D25005D3176 /* JwePasswordProtectedBackupData.swift */,
|
||||
2F848B7E282D5D25005D3176 /* JwePasswordProtectionMethod.swift */,
|
||||
);
|
||||
path = jwe;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
55011E8A2572DF1C002D2690 /* logging */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -414,6 +487,7 @@
|
|||
55575730251BC575009979AB /* VCEntities */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2F848B6F282D5D25005D3176 /* backup */,
|
||||
55B4D2D42787BB600086A9F1 /* oidc */,
|
||||
5517D28A25B90D0A00FBD239 /* exchange */,
|
||||
5573FEFD25B8DE6900282BC7 /* discovery */,
|
||||
|
@ -715,6 +789,7 @@
|
|||
92E29989252522C300B25BB6 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2F4EEF80282E758E00B62311 /* PromiseKit.framework */,
|
||||
5514E99B2609291E0004BD19 /* VCToken.framework */,
|
||||
55253975252FCA6D003202D5 /* VCCrypto.framework */,
|
||||
92E2998A252522C300B25BB6 /* VCJwt.framework */,
|
||||
|
@ -833,7 +908,9 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
55077ABA25A62DE10052C58D /* IdentifierFormatter.swift in Sources */,
|
||||
2F848B89282D5D25005D3176 /* JwePasswordProtectionMethod.swift in Sources */,
|
||||
5518CC7625264D5700C7A21B /* ResponseMappings.swift in Sources */,
|
||||
2F848B86282D5D25005D3176 /* UnprotectedBackupData.swift in Sources */,
|
||||
5573FEF525B8DE4A00282BC7 /* IONDocumentModel.swift in Sources */,
|
||||
551F3068252E97000081D5E7 /* IdentifierCreator.swift in Sources */,
|
||||
55253982252FDF89003202D5 /* KeyContainer.swift in Sources */,
|
||||
|
@ -857,6 +934,7 @@
|
|||
55B4D36927A337AD0086A9F1 /* InputDescriptorMapping.swift in Sources */,
|
||||
55B4D36727A337590086A9F1 /* NestedInputDescriptorMapping.swift in Sources */,
|
||||
5573FF0025B8DE6900282BC7 /* IdentifierDocument.swift in Sources */,
|
||||
2F848B85282D5D25005D3176 /* WalletMetadata.swift in Sources */,
|
||||
5580588725BFB6C00048E6FA /* PinDescriptor.swift in Sources */,
|
||||
5557576E251BC6CF009979AB /* ClaimDescriptor.swift in Sources */,
|
||||
5557576B251BC6CF009979AB /* ContractInputDescriptor.swift in Sources */,
|
||||
|
@ -868,11 +946,16 @@
|
|||
55575769251BC6CF009979AB /* IssuerDescriptor.swift in Sources */,
|
||||
55575778251BC6CF009979AB /* JSONCodingKeys.swift in Sources */,
|
||||
5518CC7425264CAD00C7A21B /* PresentationResponseContainer.swift in Sources */,
|
||||
2F848B80282D5D25005D3176 /* ProtectedBackupData.swift in Sources */,
|
||||
2F848B83282D5D25005D3176 /* VcMetadata.swift in Sources */,
|
||||
5518CC7225264C6F00C7A21B /* PresentationResponseFormatter.swift in Sources */,
|
||||
2F848B87282D5D25005D3176 /* BackupProtectionMethod.swift in Sources */,
|
||||
55AF7488252F6B53006A8B25 /* IdentifierDocumentSuffixDescriptor.swift in Sources */,
|
||||
55575774251BC6CF009979AB /* VCClaims.swift in Sources */,
|
||||
555BDB1C2533AA85001E7A18 /* FormatterError.swift in Sources */,
|
||||
2F848B81282D5D25005D3176 /* Microsoft2020IdentifierBackup.swift in Sources */,
|
||||
55B4D2D62787BBED0086A9F1 /* AllowedAlgorithms.swift in Sources */,
|
||||
2F848B88282D5D25005D3176 /* JwePasswordProtectedBackupData.swift in Sources */,
|
||||
553D4C472821D89D00FD39A6 /* IssuanceRequestValidator.swift in Sources */,
|
||||
5584E49925255ACA00A9DE58 /* PresentationRequestClaims.swift in Sources */,
|
||||
55575771251BC6CF009979AB /* AttestationResponseDescriptor.swift in Sources */,
|
||||
|
@ -884,6 +967,7 @@
|
|||
559FCA3C25CA03D200737F14 /* IssuanceRequest.swift in Sources */,
|
||||
55575773251BC6CF009979AB /* VerifiableCredentialDescriptor.swift in Sources */,
|
||||
55B4D36127A330680086A9F1 /* IssuanceVPFormatter.swift in Sources */,
|
||||
2F848B84282D5D25005D3176 /* Microsoft2020UnprotectedBackup.swift in Sources */,
|
||||
555CE08F2526822300C1C938 /* VerifiablePresentationDescriptor.swift in Sources */,
|
||||
55575770251BC6CF009979AB /* OIDCClaims.swift in Sources */,
|
||||
55A1EDFC27AC47FC00C63CA8 /* VPTokenResponseDescription.swift in Sources */,
|
||||
|
@ -909,6 +993,8 @@
|
|||
5531D3B2255F6B6E0002CC0E /* ResponseContaining.swift in Sources */,
|
||||
5557576A251BC6CF009979AB /* Contract.swift in Sources */,
|
||||
5517D28E25B90D0A00FBD239 /* ExchangeRequestClaims.swift in Sources */,
|
||||
2F848B82282D5D25005D3176 /* RawIdentity.swift in Sources */,
|
||||
2F848B7F282D5D25005D3176 /* UnprotectedBackup.swift in Sources */,
|
||||
559FC99025C8BEEB00737F14 /* DomainLinkageCredential.swift in Sources */,
|
||||
55379AD725A8DF7A0048600A /* PresentationRequestValidator.swift in Sources */,
|
||||
55AC02B525D3087D00AA15F4 /* IdentifierDocumentServiceEndpointDescriptor.swift in Sources */,
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Foundation
|
||||
|
||||
/// This protocol serves as the interface for any kind of backup that may hold any kind of data.
|
||||
/// This way the implementations of backups can evolve without the APIs changing.
|
||||
public protocol UnprotectedBackup : Codable { }
|
|
@ -0,0 +1,11 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/// This protocol serves as the interface for a family of objects which can apply different kinds of protection
|
||||
/// (typically, encryption) to backup data
|
||||
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,11 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/// 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 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,80 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 {
|
||||
/// For compatability with the Android backup data model:
|
||||
static let OnTheWireMainIdentifier = "did.main.identifier"
|
||||
}
|
||||
|
||||
private var entries = [RawIdentity]()
|
||||
|
||||
public init() { }
|
||||
|
||||
public init(from decoder:Decoder) throws
|
||||
{
|
||||
let container = try decoder.singleValueContainer()
|
||||
self.entries = try container.decode([RawIdentity].self)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws
|
||||
{
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode(self.entries)
|
||||
}
|
||||
|
||||
public mutating func setMain(_ identifier:Identifier) throws {
|
||||
|
||||
// Try and convert
|
||||
var entry = try RawIdentity(identifier: identifier)
|
||||
entry.name = Constants.OnTheWireMainIdentifier
|
||||
|
||||
// Remove any incidence in the array using the same identifier
|
||||
entries.removeAll(where: {$0.name == entry.name})
|
||||
|
||||
// Insert the new master identity at the head of the array
|
||||
entries.insert(entry, at: 0)
|
||||
}
|
||||
|
||||
public mutating func add(_ identifier:Identifier) throws {
|
||||
|
||||
let entry = try RawIdentity(identifier: identifier)
|
||||
entries.append(entry)
|
||||
}
|
||||
|
||||
public func all() throws -> [Identifier]
|
||||
{
|
||||
// Look for the main identifier
|
||||
var main: Identifier? = nil
|
||||
if var found = entries.first(where: {$0.name == Constants.OnTheWireMainIdentifier}) {
|
||||
found.name = AliasComputer().compute(forId: VCEntitiesConstants.MASTER_ID,
|
||||
andRelyingParty: VCEntitiesConstants.MASTER_ID)
|
||||
do {
|
||||
main = try found.asIdentifier()
|
||||
}
|
||||
catch (let error) {
|
||||
VCSDKLog.sharedInstance.logError(message: "Failed to read \(found) as the main identifier with error \(error)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Set the main identifier, if any, as the first returned
|
||||
var identifiers = [Identifier]()
|
||||
if let alpha = main {
|
||||
identifiers.append(alpha)
|
||||
}
|
||||
try entries.forEach { entry in
|
||||
// Skip the main identifier (if we encounter it again)
|
||||
if entry.name == Constants.OnTheWireMainIdentifier {
|
||||
return
|
||||
}
|
||||
try identifiers.append(entry.asIdentifier())
|
||||
}
|
||||
return identifiers
|
||||
}
|
||||
}
|
|
@ -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,105 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* 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(keyId: String, accessGroup: String?)
|
||||
case keyIdNotFound
|
||||
}
|
||||
|
||||
struct RawIdentity: Codable {
|
||||
var id: String
|
||||
var name: String
|
||||
var keys: [Jwk]?
|
||||
var recoveryKey: String
|
||||
var updateKey: String
|
||||
|
||||
init(identifier: Identifier) throws {
|
||||
|
||||
self.id = identifier.did
|
||||
self.name = identifier.alias
|
||||
self.recoveryKey = identifier.recoveryKey.keyId
|
||||
self.updateKey = identifier.updateKey.keyId
|
||||
self.keys = nil
|
||||
|
||||
var keys = try identifier.didDocumentKeys.map(jwkFromKeyContainer)
|
||||
try keys.append(jwkFromKeyContainer(identifier.recoveryKey))
|
||||
try keys.append(jwkFromKeyContainer(identifier.updateKey))
|
||||
self.keys = keys
|
||||
}
|
||||
|
||||
func asIdentifier() throws -> Identifier {
|
||||
// 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 keyContainerFromJwk(recoveryJwk)
|
||||
let updateKeyContainer = try keyContainerFromJwk(updateJwk)
|
||||
let signingKeyContainer = try keyContainerFromJwk(signingJwk)
|
||||
|
||||
// Wrap up and return
|
||||
return Identifier(longFormDid: self.id,
|
||||
didDocumentKeys: [signingKeyContainer],
|
||||
updateKey: updateKeyContainer,
|
||||
recoveryKey: recoveryKeyContainer,
|
||||
alias: self.name)
|
||||
}
|
||||
|
||||
func jwkFromKeyContainer(_ keyContainer: KeyContainer) throws -> Jwk {
|
||||
|
||||
// Get out the private and public components of the key (pair)
|
||||
let secret = keyContainer.keyReference
|
||||
let (privateKey, publicKey) = try Secp256k1().createKeyPair(forSecret: secret)
|
||||
if privateKey.value.isEmpty {
|
||||
throw RawIdentityError.privateKeyNotFound(keyId: secret.id.uuidString,
|
||||
accessGroup: secret.accessGroup)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
func keyContainerFromJwk(_ jwk: Jwk) throws -> KeyContainer {
|
||||
|
||||
// Get out the ID and the key data
|
||||
guard let keyId = jwk.keyId else {
|
||||
throw RawIdentityError.keyIdNotFound
|
||||
}
|
||||
guard let privateKeyData = jwk.d,
|
||||
!privateKeyData.isEmpty else {
|
||||
throw RawIdentityError.privateKeyNotFound(keyId: keyId, accessGroup: nil)
|
||||
}
|
||||
|
||||
// Wrap it all up
|
||||
let privateKey = EphemeralSecret(with: privateKeyData,
|
||||
id: UUID(),
|
||||
accessGroup: VCSDKConfiguration.sharedInstance.accessGroupIdentifier)
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -97,6 +97,7 @@ public class IssuanceResponseFormatter: IssuanceResponseFormatting {
|
|||
|
||||
sdkLog.logVerbose(message: """
|
||||
Creating Issuance Response with:
|
||||
access_tokens: \(accessTokenMap?.count ?? 0)
|
||||
id_tokens: \(idTokenMap?.count ?? 0)
|
||||
self_issued claims: \(selfIssuedMap?.count ?? 0)
|
||||
verifiable credentials: \(presentationsMap?.count ?? 0)
|
||||
|
|
|
@ -7,15 +7,15 @@ import VCCrypto
|
|||
|
||||
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"
|
||||
|
||||
/// key reference to key in Secret Store
|
||||
public let keyReference: VCCryptoSecret
|
||||
|
||||
/// 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
|
||||
|
@ -26,8 +26,8 @@ public struct KeyContainer {
|
|||
return keyReference.id
|
||||
}
|
||||
|
||||
public func isValidKey() -> Bool {
|
||||
return keyReference.isValidKey()
|
||||
public func isValidKey() throws {
|
||||
try keyReference.isValidKey()
|
||||
}
|
||||
|
||||
/// Migrate key from old access group to new one set in sdk config.
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -93,7 +93,7 @@ public struct VCSDKLog {
|
|||
}
|
||||
}
|
||||
|
||||
func event(name: String, properties: [String: String]? = nil, measurements: [String: NSNumber]? = nil) {
|
||||
public func event(name: String, properties: [String: String]? = nil, measurements: [String: NSNumber]? = nil) {
|
||||
consumers.forEach { logger in
|
||||
logger.event(name: name, properties: properties, measurements: measurements)
|
||||
}
|
||||
|
|
|
@ -10,10 +10,15 @@ import VCToken
|
|||
|
||||
struct MockCryptoOperations: CryptoOperating {
|
||||
|
||||
static let itemTypeCode = "r32b"
|
||||
static let accessGroupIdentifier = "anywhere.com.microsoft.azureauthenticator.did"
|
||||
|
||||
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 +30,42 @@ struct MockCryptoOperations: CryptoOperating {
|
|||
func retrieveKeyFromStorage(withId id: UUID) -> VCCryptoSecret {
|
||||
return KeyId(id: id)
|
||||
}
|
||||
|
||||
public func save(key: Data, withId id: UUID) throws {
|
||||
|
||||
// Take a copy of the key to let the store dispose of it
|
||||
var data = Data()
|
||||
data.append(key)
|
||||
|
||||
// Store down
|
||||
try secretStore.saveSecret(id: id,
|
||||
itemTypeCode: Self.itemTypeCode,
|
||||
accessGroup: Self.accessGroupIdentifier,
|
||||
value: &data)
|
||||
}
|
||||
|
||||
public func deleteKey(withId id: UUID) throws {
|
||||
|
||||
let itemTypeCode = Self.itemTypeCode
|
||||
let accessGroup = Self.accessGroupIdentifier
|
||||
do {
|
||||
let _ = try secretStore.getSecret(id: id,
|
||||
itemTypeCode: itemTypeCode,
|
||||
accessGroup: accessGroup)
|
||||
|
||||
// If we get here the key exists, so we can delete it
|
||||
try secretStore.deleteSecret(id: id, itemTypeCode: itemTypeCode, accessGroup: accessGroup)
|
||||
}
|
||||
catch SecretStoringError.itemNotFound {
|
||||
/* There's no key so nothing to delete */
|
||||
}
|
||||
}
|
||||
|
||||
public func getKey(withId id: UUID) throws -> Data {
|
||||
|
||||
return try secretStore.getSecret(id: id,
|
||||
itemTypeCode: Self.itemTypeCode,
|
||||
accessGroup: Self.accessGroupIdentifier)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,9 +16,9 @@ struct MockCryptoSecret: VCCryptoSecret {
|
|||
self.id = id
|
||||
}
|
||||
|
||||
func isValidKey() -> Bool {
|
||||
return true
|
||||
}
|
||||
func isValidKey() throws { }
|
||||
|
||||
func migrateKey(fromAccessGroup oldAccessGroup: String?) throws { }
|
||||
|
||||
func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> Void) throws { }
|
||||
}
|
||||
|
|
|
@ -21,4 +21,5 @@ internal class SecretStoreMock: SecretStoring {
|
|||
func deleteSecret(id: UUID, itemTypeCode: String, accessGroup: String?) throws {
|
||||
memoryStore.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -7,6 +7,9 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
2F848B52282D4DEC005D3176 /* difwordlist.txt in Resources */ = {isa = PBXBuildFile; fileRef = 2F848B51282D4DEC005D3176 /* difwordlist.txt */; };
|
||||
2F848B54282D4EF3005D3176 /* DifWordListTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F848B53282D4EF3005D3176 /* DifWordListTests.swift */; };
|
||||
2F848B56282D4F1F005D3176 /* DifWordList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F848B55282D4F1F005D3176 /* DifWordList.swift */; };
|
||||
550F1E3F25101759009AF467 /* VCServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 550F1E3525101758009AF467 /* VCServices.framework */; };
|
||||
550F1E4425101759009AF467 /* IssuanceServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 550F1E4325101759009AF467 /* IssuanceServiceTests.swift */; };
|
||||
550F1E4625101759009AF467 /* VCUseCase.h in Headers */ = {isa = PBXBuildFile; fileRef = 550F1E3825101758009AF467 /* VCUseCase.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
|
@ -36,13 +39,14 @@
|
|||
55793BD7255C5A26007F7599 /* ExchangeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55793BD6255C5A25007F7599 /* ExchangeService.swift */; };
|
||||
55793BDB255C654E007F7599 /* MockExchangeRequestFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55793BDA255C654E007F7599 /* MockExchangeRequestFormatter.swift */; };
|
||||
55793BDD255C65DA007F7599 /* ExchangeServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55793BDC255C65DA007F7599 /* ExchangeServiceTests.swift */; };
|
||||
5580583325BF97DF0048E6FA /* VerifiableCredentialDataModel.xcdatamodeld in Resources */ = {isa = PBXBuildFile; fileRef = 555BDAEA2530AEC7001E7A18 /* VerifiableCredentialDataModel.xcdatamodeld */; };
|
||||
5580583E25BFA2140048E6FA /* ServicesConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5580583D25BFA2140048E6FA /* ServicesConstants.swift */; };
|
||||
559FC9D225C9D90B00737F14 /* LinkedDomainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559FC9D125C9D90B00737F14 /* LinkedDomainService.swift */; };
|
||||
559FC9F125C9F2B700737F14 /* LinkedDomainServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559FC9F025C9F2B700737F14 /* LinkedDomainServiceTests.swift */; };
|
||||
559FC9F925C9F35A00737F14 /* MockWellKnownConfigDocumentApiCalls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559FC9F825C9F35A00737F14 /* MockWellKnownConfigDocumentApiCalls.swift */; };
|
||||
559FCA3425C9FB7D00737F14 /* MockDomainLinkageCredentialValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559FCA3325C9FB7D00737F14 /* MockDomainLinkageCredentialValidator.swift */; };
|
||||
55AF7490252FBF5B006A8B25 /* MockVCCryptoSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55AF748F252FBF5B006A8B25 /* MockVCCryptoSecret.swift */; };
|
||||
55BA803B2832C91B004C532A /* VCServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 550F1E3525101758009AF467 /* VCServices.framework */; };
|
||||
55BA803D2832CDE0004C532A /* VCEntities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 55BA803C2832CDE0004C532A /* VCEntities.framework */; };
|
||||
55BF81652829B73600B61D33 /* MockIssuanceRequestValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF81642829B73600B61D33 /* MockIssuanceRequestValidator.swift */; };
|
||||
55CA3A7F27FB60C900615CB6 /* VCSDKConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55CA3A7E27FB60C900615CB6 /* VCSDKConfiguration.swift */; };
|
||||
55DA76ED25BE36FF009C32E0 /* MockIssuanceApiCalls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DA76EC25BE36FF009C32E0 /* MockIssuanceApiCalls.swift */; };
|
||||
|
@ -74,6 +78,9 @@
|
|||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
2F848B51282D4DEC005D3176 /* difwordlist.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = difwordlist.txt; sourceTree = "<group>"; };
|
||||
2F848B53282D4EF3005D3176 /* DifWordListTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DifWordListTests.swift; sourceTree = "<group>"; };
|
||||
2F848B55282D4F1F005D3176 /* DifWordList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DifWordList.swift; sourceTree = "<group>"; };
|
||||
550F1E3525101758009AF467 /* VCServices.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = VCServices.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
550F1E3825101758009AF467 /* VCUseCase.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VCUseCase.h; sourceTree = "<group>"; };
|
||||
550F1E3925101758009AF467 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
|
@ -114,6 +121,10 @@
|
|||
559FC9F825C9F35A00737F14 /* MockWellKnownConfigDocumentApiCalls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockWellKnownConfigDocumentApiCalls.swift; sourceTree = "<group>"; };
|
||||
559FCA3325C9FB7D00737F14 /* MockDomainLinkageCredentialValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDomainLinkageCredentialValidator.swift; sourceTree = "<group>"; };
|
||||
55AF748F252FBF5B006A8B25 /* MockVCCryptoSecret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockVCCryptoSecret.swift; sourceTree = "<group>"; };
|
||||
55BA80352832C900004C532A /* VCCrypto.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = VCCrypto.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
55BA80372832C900004C532A /* VCEntities.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = VCEntities.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
55BA80392832C900004C532A /* VCNetworking.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = VCNetworking.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
55BA803C2832CDE0004C532A /* VCEntities.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = VCEntities.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
55BF81642829B73600B61D33 /* MockIssuanceRequestValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockIssuanceRequestValidator.swift; sourceTree = "<group>"; };
|
||||
55CA3A7E27FB60C900615CB6 /* VCSDKConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = VCSDKConfiguration.swift; path = ../../VCEntities/VCEntities/util/VCSDKConfiguration.swift; sourceTree = "<group>"; };
|
||||
55DA76EC25BE36FF009C32E0 /* MockIssuanceApiCalls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockIssuanceApiCalls.swift; sourceTree = "<group>"; };
|
||||
|
@ -152,12 +163,22 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
55BA803D2832CDE0004C532A /* VCEntities.framework in Frameworks */,
|
||||
55BA803B2832C91B004C532A /* VCServices.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
2F848B50282D4DEC005D3176 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2F848B51282D4DEC005D3176 /* difwordlist.txt */,
|
||||
);
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
550F1E2B25101758009AF467 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -182,7 +203,9 @@
|
|||
550F1E3725101758009AF467 /* VCServices */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2F848B50282D4DEC005D3176 /* Resources */,
|
||||
552539832530AE5F003202D5 /* coreData */,
|
||||
2F848B55282D4F1F005D3176 /* DifWordList.swift */,
|
||||
555BDB142530F024001E7A18 /* VerifiableCredentialSDK.swift */,
|
||||
5531D3AD255F1F360002CC0E /* IdentifierService.swift */,
|
||||
550F1E532510195A009AF467 /* IssuanceService.swift */,
|
||||
|
@ -202,6 +225,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
550F1E64251165DB009AF467 /* mocks */,
|
||||
2F848B53282D4EF3005D3176 /* DifWordListTests.swift */,
|
||||
550F1E4325101759009AF467 /* IssuanceServiceTests.swift */,
|
||||
55793BDC255C65DA007F7599 /* ExchangeServiceTests.swift */,
|
||||
559FC9F025C9F2B700737F14 /* LinkedDomainServiceTests.swift */,
|
||||
|
@ -263,6 +287,10 @@
|
|||
8A1DACB8079028BD52F83396 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
55BA803C2832CDE0004C532A /* VCEntities.framework */,
|
||||
55BA80352832C900004C532A /* VCCrypto.framework */,
|
||||
55BA80372832C900004C532A /* VCEntities.framework */,
|
||||
55BA80392832C900004C532A /* VCNetworking.framework */,
|
||||
92D5988725C23E4E00C42370 /* Secp256k1.framework */,
|
||||
92D5988525C23E2A00C42370 /* VCNetworking.framework */,
|
||||
92F2ED2825B9EB2F00A6911C /* VCRepository.framework */,
|
||||
|
@ -388,6 +416,7 @@
|
|||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
2F848B52282D4DEC005D3176 /* difwordlist.txt in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -395,7 +424,6 @@
|
|||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5580583325BF97DF0048E6FA /* VerifiableCredentialDataModel.xcdatamodeld in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -427,6 +455,7 @@
|
|||
555BDAEE2530CF27001E7A18 /* CoreDataManager.swift in Sources */,
|
||||
5531D3AE255F1F360002CC0E /* IdentifierService.swift in Sources */,
|
||||
5580583E25BFA2140048E6FA /* ServicesConstants.swift in Sources */,
|
||||
2F848B56282D4F1F005D3176 /* DifWordList.swift in Sources */,
|
||||
555CE0932526AD0400C1C938 /* PresentationService.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -440,6 +469,7 @@
|
|||
550F1E6625116CD6009AF467 /* TestData.swift in Sources */,
|
||||
551F304F2527EAF40081D5E7 /* PresentationServiceTests.swift in Sources */,
|
||||
55DA76ED25BE36FF009C32E0 /* MockIssuanceApiCalls.swift in Sources */,
|
||||
2F848B54282D4EF3005D3176 /* DifWordListTests.swift in Sources */,
|
||||
55DA770525BE45C7009C32E0 /* MockDiscoveryApiCalls.swift in Sources */,
|
||||
55DA76FA25BE390E009C32E0 /* MockPresentationApiCalls.swift in Sources */,
|
||||
555BDB132530EA79001E7A18 /* IdentifierDatabaseTests.swift in Sources */,
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 class DifWordList {
|
||||
|
||||
let words: [String]
|
||||
|
||||
public init?() throws {
|
||||
|
||||
if let path = Bundle(for: Self.self).path(forResource: "difwordlist", ofType: "txt") {
|
||||
let content = try String(contentsOfFile: path, encoding: .utf8)
|
||||
self.words = content.components(separatedBy: .whitespacesAndNewlines).filter{ !($0.isEmpty) }
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Selects a word at random from the list and returns it
|
||||
public func randomWord() -> String {
|
||||
let index = arc4random_uniform(UInt32(self.words.count))
|
||||
return self.words[Int(index)]
|
||||
}
|
||||
|
||||
/// Randomly selects a specified number of words from the list
|
||||
/// - Parameters:
|
||||
/// - count: The number of words to select
|
||||
/// - Returns: An array of words selected at random from the list up to lesser of the specified number or the number of words in the list
|
||||
public func randomWords(count:UInt32) -> [String] {
|
||||
|
||||
// Look for an early out
|
||||
if count == 0 {
|
||||
return [String]()
|
||||
}
|
||||
if count >= UInt32(self.words.count) {
|
||||
// We need to return all the words in the list
|
||||
// so we take a copy and randomly shuffle it
|
||||
return self.words.shuffled()
|
||||
}
|
||||
|
||||
var list = [String]()
|
||||
var set = Set<String>()
|
||||
repeat {
|
||||
// Get another word
|
||||
let word = self.randomWord()
|
||||
|
||||
// Was it already selected?
|
||||
if set.contains(word) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Nope
|
||||
set.insert(word)
|
||||
list.append(word)
|
||||
} while (list.count < count)
|
||||
return list
|
||||
}
|
||||
|
||||
public static func normalize(password:String) -> String {
|
||||
|
||||
var list = [String]()
|
||||
|
||||
// Split the input string along whitespace, etc, and accumulate
|
||||
// each non-empty substring
|
||||
let components = password.components(separatedBy: .whitespacesAndNewlines)
|
||||
components.forEach { component in
|
||||
if component.isEmpty {
|
||||
return
|
||||
}
|
||||
list.append(component.lowercased())
|
||||
}
|
||||
|
||||
return list.joined(separator: " ")
|
||||
}
|
||||
}
|
|
@ -6,6 +6,12 @@
|
|||
import VCEntities
|
||||
import VCCrypto
|
||||
|
||||
public enum IdentifierServiceError: Error {
|
||||
case keyNotFoundInKeyStore(innerError: Error)
|
||||
case keyStoreError(message: String)
|
||||
case noKeysSavedForIdentifier
|
||||
}
|
||||
|
||||
public class IdentifierService {
|
||||
|
||||
private let identifierDB: IdentifierDatabase
|
||||
|
@ -32,6 +38,63 @@ public class IdentifierService {
|
|||
return try identifierDB.fetchMasterIdentifier()
|
||||
}
|
||||
|
||||
func fetchOrCreateMasterIdentifier() throws -> Identifier {
|
||||
|
||||
let identifier: Identifier
|
||||
do {
|
||||
identifier = try fetchMasterIdentifier()
|
||||
} catch {
|
||||
sdkLog.logError(message: "Master identifier does not exist with error: \(String(describing: error))")
|
||||
sdkLog.logInfo(message: "Creating new Identifier.")
|
||||
return try createMasterIdentifier()
|
||||
}
|
||||
|
||||
do {
|
||||
try validateAndHandleKeys(for: identifier)
|
||||
return identifier
|
||||
} catch {
|
||||
/// in rare chance no keys are saved, create a new identifier.
|
||||
if case IdentifierServiceError.noKeysSavedForIdentifier = error {
|
||||
return try refreshIdentifiers()
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createMasterIdentifier() throws -> Identifier {
|
||||
try createAndSaveIdentifier(forId: VCEntitiesConstants.MASTER_ID, andRelyingParty: VCEntitiesConstants.MASTER_ID)
|
||||
}
|
||||
|
||||
private func refreshIdentifiers() throws -> Identifier {
|
||||
try identifierDB.removeAllIdentifiers()
|
||||
let identifier = try createMasterIdentifier()
|
||||
sdkLog.logInfo(message: "Refreshed identifier.")
|
||||
return identifier
|
||||
}
|
||||
|
||||
public func fetchIdentifiersForExport() throws -> [Identifier] {
|
||||
return try identifierDB.fetchAllIdentifiers()
|
||||
}
|
||||
|
||||
public func replaceIdentifiers(with identifiers:[Identifier]) throws {
|
||||
|
||||
try identifierDB.removeAllIdentifiers()
|
||||
try identifiers.forEach { identifier in
|
||||
try identifierDB.importIdentifier(identifier: identifier)
|
||||
|
||||
// Ensure the keys are migrated into the correct access group
|
||||
let alias = identifier.alias
|
||||
do {
|
||||
let fetched = try identifierDB.fetchIdentifier(withAlias: alias)
|
||||
try migrateKeys(in: fetched, fromAccessGroup: nil)
|
||||
}
|
||||
catch {
|
||||
sdkLog.logWarning(message: "Failed to migrate keys in imported identifier w/alias: \(alias)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchIdentifier(withAlias alias: String) throws -> Identifier {
|
||||
return try identifierDB.fetchIdentifier(withAlias: alias)
|
||||
}
|
||||
|
@ -54,9 +117,44 @@ public class IdentifierService {
|
|||
return identifier
|
||||
}
|
||||
|
||||
private func validateAndHandleKeys(for identifier: Identifier) throws
|
||||
{
|
||||
do {
|
||||
/// Step 1: Check to see if keys are valid.
|
||||
try areKeysValid(for: identifier)
|
||||
} catch {
|
||||
|
||||
/// Step 2: If keys are not valid, check to see if we need to migrate keys.
|
||||
if case IdentifierServiceError.keyNotFoundInKeyStore = error {
|
||||
try migrateKeys()
|
||||
return
|
||||
}
|
||||
|
||||
/// Else, throw error.
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/// Migrate keys from default access group to new access group.
|
||||
private func migrateKeys() throws
|
||||
{
|
||||
do {
|
||||
/// Step 3: migrate keys.
|
||||
try migrateKeys(fromAccessGroup: nil)
|
||||
sdkLog.logInfo(message: "Keys successfully migrated to new access group.")
|
||||
} catch {
|
||||
sdkLog.logError(message: "failed to migrate keys to new access group with error: \(String(describing: error))")
|
||||
throw IdentifierServiceError.noKeysSavedForIdentifier
|
||||
}
|
||||
}
|
||||
|
||||
/// updates access group for keys if it needs to be updated.
|
||||
public func migrateKeys(fromAccessGroup currentAccessGroup: String?) throws {
|
||||
private func migrateKeys(fromAccessGroup currentAccessGroup: String?) throws {
|
||||
let identifier = try fetchMasterIdentifier()
|
||||
try migrateKeys(in: identifier, fromAccessGroup: currentAccessGroup)
|
||||
}
|
||||
|
||||
private func migrateKeys(in identifier: Identifier, fromAccessGroup currentAccessGroup: String?) throws {
|
||||
try identifier.recoveryKey.migrateKey(fromAccessGroup: currentAccessGroup)
|
||||
try identifier.updateKey.migrateKey(fromAccessGroup: currentAccessGroup)
|
||||
try identifier.didDocumentKeys.forEach { keyContainer in
|
||||
|
@ -64,10 +162,27 @@ public class IdentifierService {
|
|||
}
|
||||
}
|
||||
|
||||
public func areKeysValid() throws -> Bool {
|
||||
let identifier = try fetchMasterIdentifier()
|
||||
return identifier.recoveryKey.isValidKey() &&
|
||||
identifier.updateKey.isValidKey() &&
|
||||
(identifier.didDocumentKeys.first?.isValidKey() ?? false)
|
||||
private func areKeysValid(for identifier: Identifier) throws {
|
||||
do {
|
||||
try identifier.recoveryKey.isValidKey()
|
||||
try identifier.updateKey.isValidKey()
|
||||
|
||||
for key in identifier.didDocumentKeys {
|
||||
try key.isValidKey()
|
||||
}
|
||||
}
|
||||
catch {
|
||||
|
||||
if case SecretStoringError.itemNotFound = error {
|
||||
throw IdentifierServiceError.keyNotFoundInKeyStore(innerError: error)
|
||||
}
|
||||
|
||||
if case SecretStoringError.readFromStoreError(status: _, message: let message) = error {
|
||||
throw IdentifierServiceError.keyStoreError(message: message ?? "Unable to retrieve keys")
|
||||
}
|
||||
|
||||
/// rethrow error if not key not found error.
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -88,7 +88,7 @@ public class IssuanceService {
|
|||
/// turn off pairwise until we have a better solution.
|
||||
self.exchangeVCsIfPairwise(response: response, isPairwise: false)
|
||||
}.then { response in
|
||||
self.formatIssuanceResponse(response: response, isPairwise: false)
|
||||
self.formatIssuanceResponse(response: response)
|
||||
}.then { signedToken in
|
||||
self.apiCalls.sendResponse(usingUrl: response.audienceUrl, withBody: signedToken)
|
||||
}
|
||||
|
@ -157,26 +157,14 @@ public class IssuanceService {
|
|||
}
|
||||
}
|
||||
|
||||
private func formatIssuanceResponse(response: IssuanceResponseContainer, isPairwise: Bool) -> Promise<IssuanceResponse> {
|
||||
private func formatIssuanceResponse(response: IssuanceResponseContainer) -> Promise<IssuanceResponse> {
|
||||
return Promise { seal in
|
||||
do {
|
||||
/// fetch or create master identifier
|
||||
let identifier = try identifierService.fetchOrCreateMasterIdentifier()
|
||||
sdkLog.logVerbose(message: "Signing Issuance Response with Identifier")
|
||||
|
||||
var identifier: Identifier?
|
||||
|
||||
if isPairwise {
|
||||
// TODO: will change when deterministic key generation is implemented.
|
||||
identifier = try identifierService.fetchIdentifier(forId: VCEntitiesConstants.MASTER_ID, andRelyingParty: response.audienceDid)
|
||||
} else {
|
||||
identifier = try identifierService.fetchMasterIdentifier()
|
||||
}
|
||||
|
||||
guard let id = identifier else {
|
||||
throw IssuanceServiceError.unableToFetchIdentifier
|
||||
}
|
||||
|
||||
sdkLog.logInfo(message: "Signing Issuance Response with Identifier")
|
||||
|
||||
seal.fulfill(try self.formatter.format(response: response, usingIdentifier: id))
|
||||
seal.fulfill(try self.formatter.format(response: response, usingIdentifier: identifier))
|
||||
} catch {
|
||||
seal.reject(error)
|
||||
}
|
||||
|
|
|
@ -9,10 +9,12 @@ import VCEntities
|
|||
|
||||
enum PresentationServiceError: Error {
|
||||
case inputStringNotUri
|
||||
case noKeysSavedForIdentifier
|
||||
case noQueryParametersOnUri
|
||||
case noValueForRequestUriQueryParameter
|
||||
case noRequestUriQueryParameter
|
||||
case unableToCastToPresentationResponseContainer
|
||||
case unableToFetchIdentifier
|
||||
case noKeyIdInRequestHeader
|
||||
case noPublicKeysInIdentifierDocument
|
||||
case noIssuerIdentifierInRequest
|
||||
|
@ -81,7 +83,7 @@ public class PresentationService {
|
|||
/// turn off pairwise until we have a better solution.
|
||||
self.exchangeVCsIfPairwise(response: response, isPairwise: false)
|
||||
}.then { response in
|
||||
self.formatPresentationResponse(response: response, isPairwise: false)
|
||||
self.formatPresentationResponse(response: response)
|
||||
}.then { signedToken in
|
||||
self.presentationApiCalls.sendResponse(usingUrl: response.audienceUrl, withBody: signedToken)
|
||||
}
|
||||
|
@ -197,26 +199,14 @@ public class PresentationService {
|
|||
}
|
||||
}
|
||||
|
||||
private func formatPresentationResponse(response: PresentationResponseContainer, isPairwise: Bool) -> Promise<PresentationResponse> {
|
||||
private func formatPresentationResponse(response: PresentationResponseContainer) -> Promise<PresentationResponse> {
|
||||
return Promise { seal in
|
||||
do {
|
||||
|
||||
var identifier: Identifier?
|
||||
let identifier = try identifierService.fetchOrCreateMasterIdentifier()
|
||||
sdkLog.logVerbose(message: "Signing Presentation Response with Identifier")
|
||||
|
||||
if isPairwise {
|
||||
// TODO: will change when deterministic key generation is implemented.
|
||||
identifier = try identifierService.fetchIdentifier(forId: VCEntitiesConstants.MASTER_ID, andRelyingParty: response.audienceDid)
|
||||
} else {
|
||||
identifier = try identifierService.fetchMasterIdentifier()
|
||||
}
|
||||
|
||||
guard let id = identifier else {
|
||||
throw PresentationServiceError.inputStringNotUri
|
||||
}
|
||||
|
||||
sdkLog.logInfo(message: "Signing Presentation Response with Identifier")
|
||||
|
||||
seal.fulfill(try self.formatter.format(response: response, usingIdentifier: id))
|
||||
seal.fulfill(try self.formatter.format(response: response, usingIdentifier: identifier))
|
||||
} catch {
|
||||
seal.reject(error)
|
||||
}
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -8,61 +8,29 @@ import VCEntities
|
|||
/// status of a successful initialization
|
||||
public enum VCSDKInitStatus
|
||||
{
|
||||
case newMasterIdentifierCreated
|
||||
case success
|
||||
}
|
||||
|
||||
/// initialization errors.
|
||||
public enum VCSDKInitError: Error {
|
||||
case invalidKeys
|
||||
}
|
||||
|
||||
/// Class used to Initialize the SDK.
|
||||
public class VerifiableCredentialSDK {
|
||||
|
||||
public static let identifierService = IdentifierService()
|
||||
|
||||
/// Initialized the SDK.
|
||||
/// Returns: TRUE, if needed to create Master Identifier
|
||||
/// FALSE, if Master Identifier is able to be fetched (included private keys from KeyStore)
|
||||
/// Returns: Result<VCSDKInitStatus>, if successfully initialized the SDK.
|
||||
/// Result<Error>, if there was an error, and unable to initialize SDK.
|
||||
public static func initialize(logConsumer: VCLogConsumer = DefaultVCLogConsumer(),
|
||||
accessGroupIdentifier: String? = nil) -> Result<VCSDKInitStatus, Error> {
|
||||
|
||||
/// Step 1: Add Log to VCSDKLog shared instance.
|
||||
VCSDKLog.sharedInstance.add(consumer: logConsumer)
|
||||
|
||||
/// Get access group identifier for app.
|
||||
/// Step 2: Set access group identifier for key chain.
|
||||
if let accessGroupIdentifier = accessGroupIdentifier {
|
||||
VCSDKConfiguration.sharedInstance.setAccessGroupIdentifier(with: accessGroupIdentifier)
|
||||
}
|
||||
|
||||
/// Try to fetch master identifier from storage.
|
||||
do {
|
||||
|
||||
_ = try identifierService.fetchMasterIdentifier()
|
||||
|
||||
if try !identifierService.areKeysValid() {
|
||||
return .failure(VCSDKInitError.invalidKeys)
|
||||
}
|
||||
|
||||
} catch {
|
||||
|
||||
/// If unable to fetch master identifier, create a new one.
|
||||
VCSDKLog.sharedInstance.logWarning(message: "Failed to fetch master identifier with: \(String(describing: error))")
|
||||
return createNewIdentifier()
|
||||
|
||||
}
|
||||
|
||||
/// VC sdk initialization successful.
|
||||
/// Step 4: return success.
|
||||
return .success(.success)
|
||||
}
|
||||
|
||||
private static func createNewIdentifier() -> Result<VCSDKInitStatus, Error> {
|
||||
do {
|
||||
_ = try identifierService.createAndSaveIdentifier(forId: VCEntitiesConstants.MASTER_ID,
|
||||
andRelyingParty: VCEntitiesConstants.MASTER_ID)
|
||||
return .success(.newMasterIdentifierCreated)
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -32,7 +32,6 @@ struct IdentifierDatabase {
|
|||
|
||||
func saveIdentifier(identifier: Identifier) throws {
|
||||
|
||||
|
||||
/// signing key is always first key in DID document keys until we implement more complex registration scenario.
|
||||
guard let signingKey = identifier.didDocumentKeys.first else {
|
||||
throw IdentifierDatabaseError.unableToSaveIdentifier
|
||||
|
@ -42,7 +41,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 +90,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 {
|
||||
try cryptoOperations.deleteKey(withId: uuid)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
let secret = try EphemeralSecret(with: keyContainer.keyReference)
|
||||
try cryptoOperations.save(key: secret.value, withId: keyContainer.keyReference.id)
|
||||
}
|
||||
|
||||
try self.saveIdentifier(identifier: identifier)
|
||||
}
|
||||
|
||||
private func createIdentifier(fromIdentifierModel model: IdentifierModel) throws -> Identifier {
|
||||
|
||||
guard let longFormDid = model.did,
|
||||
|
@ -95,9 +131,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>
|
|
@ -0,0 +1,61 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import VCServices
|
||||
|
||||
class DifWordListTests: XCTestCase {
|
||||
|
||||
var fixture: DifWordList? = nil
|
||||
|
||||
override func setUpWithError() throws {
|
||||
self.fixture = try DifWordList()
|
||||
}
|
||||
|
||||
func testDifWordListRandomSelection() throws {
|
||||
|
||||
let count = 12;
|
||||
guard let words = fixture?.randomWords(count: UInt32(count)) else {
|
||||
XCTFail("Failed to generate a random sequence of words using \(String(describing: fixture))")
|
||||
return
|
||||
}
|
||||
NSLog("Words: %@", words)
|
||||
|
||||
// Validate that the returned list contains the specified number of words
|
||||
// with no repetition
|
||||
let set = Set(words)
|
||||
XCTAssertEqual(set.count, count, "Unexpected number of words in generated password")
|
||||
}
|
||||
|
||||
func testDifWordsListUpperBounds() throws {
|
||||
|
||||
// Get all the words from the list
|
||||
guard let words1 = fixture?.randomWords(count: UInt32.max) else {
|
||||
XCTFail("Failed to generate a random sequence of words using \(String(describing: fixture))")
|
||||
return
|
||||
}
|
||||
|
||||
// Do it again
|
||||
guard let words2 = fixture?.randomWords(count: UInt32.max) else {
|
||||
XCTFail("Failed to generate a random sequence of words using \(String(describing: fixture))")
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure that they are not the same
|
||||
let separator = " "
|
||||
XCTAssertNotEqual(words1.joined(separator: separator), words2.joined(separator: separator))
|
||||
}
|
||||
|
||||
func testDifWordListLowerBounds() throws {
|
||||
|
||||
let zero = 0
|
||||
guard let words = fixture?.randomWords(count: UInt32(zero)) else {
|
||||
XCTFail("Failed to generate an empty sequence of words using \(String(describing: fixture))")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(words.count, zero)
|
||||
}
|
||||
}
|
|
@ -9,11 +9,11 @@ struct MockVCCryptoSecret: VCCryptoSecret {
|
|||
|
||||
var accessGroup: String? = nil
|
||||
|
||||
func isValidKey() -> Bool {
|
||||
return true
|
||||
}
|
||||
func isValidKey() throws { }
|
||||
|
||||
func migrateKey(fromAccessGroup oldAccessGroup: String?) throws { }
|
||||
|
||||
var id: UUID = UUID()
|
||||
|
||||
func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> Void) throws { }
|
||||
}
|
||||
|
|
|
@ -23,4 +23,5 @@ internal class SecretStoreMock: SecretStoring {
|
|||
print("deletingSecret... " + id.uuidString)
|
||||
memoryStore.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,6 +7,11 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
2F848B66282D5236005D3176 /* PbesJwe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F848B65282D5236005D3176 /* PbesJwe.swift */; };
|
||||
2F848B6B282D5266005D3176 /* JweEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F848B67282D5266005D3176 /* JweEncoder.swift */; };
|
||||
2F848B6C282D5266005D3176 /* Jwk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F848B68282D5266005D3176 /* Jwk.swift */; };
|
||||
2F848B6D282D5266005D3176 /* JweToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F848B69282D5266005D3176 /* JweToken.swift */; };
|
||||
2F848B6E282D5266005D3176 /* JweDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F848B6A282D5266005D3176 /* JweDecoder.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 */
|
||||
2F848B65282D5236005D3176 /* PbesJwe.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PbesJwe.swift; sourceTree = "<group>"; };
|
||||
2F848B67282D5266005D3176 /* JweEncoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JweEncoder.swift; sourceTree = "<group>"; };
|
||||
2F848B68282D5266005D3176 /* Jwk.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Jwk.swift; sourceTree = "<group>"; };
|
||||
2F848B69282D5266005D3176 /* JweToken.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JweToken.swift; sourceTree = "<group>"; };
|
||||
2F848B6A282D5266005D3176 /* JweDecoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JweDecoder.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 */,
|
||||
2F848B65282D5236005D3176 /* PbesJwe.swift */,
|
||||
);
|
||||
path = algorithms;
|
||||
sourceTree = "<group>";
|
||||
|
@ -128,6 +139,10 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
5526934F24FF32850051A358 /* algorithms */,
|
||||
2F848B6A282D5266005D3176 /* JweDecoder.swift */,
|
||||
2F848B67282D5266005D3176 /* JweEncoder.swift */,
|
||||
2F848B69282D5266005D3176 /* JweToken.swift */,
|
||||
2F848B68282D5266005D3176 /* Jwk.swift */,
|
||||
55ADE27824F540F700D9990E /* JwsToken.swift */,
|
||||
55ADE27C24F5410E00D9990E /* Claims.swift */,
|
||||
55ADE27124F5407D00D9990E /* Header.swift */,
|
||||
|
@ -286,17 +301,22 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
2F848B6D282D5266005D3176 /* JweToken.swift in Sources */,
|
||||
55ADE27D24F5410E00D9990E /* Claims.swift in Sources */,
|
||||
5540904B2500311E001246DB /* ECPublicJwk.swift in Sources */,
|
||||
55C6C4A6250816500082BE73 /* Secp256k1Verifier.swift in Sources */,
|
||||
55ADE27924F540F700D9990E /* JwsToken.swift in Sources */,
|
||||
2F848B6C282D5266005D3176 /* Jwk.swift in Sources */,
|
||||
55ADE27224F5407D00D9990E /* Header.swift in Sources */,
|
||||
2F848B6B282D5266005D3176 /* JweEncoder.swift in Sources */,
|
||||
55269351250006020051A358 /* VCTokenError.swift in Sources */,
|
||||
55ADE28524F5412900D9990E /* TokenVerifying.swift in Sources */,
|
||||
55ADE28124F5412100D9990E /* JwsDecoder.swift in Sources */,
|
||||
550F1E56251019BA009AF467 /* KeyId.swift in Sources */,
|
||||
55ADE27F24F5411E00D9990E /* JwsEncoder.swift in Sources */,
|
||||
2F848B6E282D5266005D3176 /* JweDecoder.swift in Sources */,
|
||||
55ADE27B24F5410400D9990E /* Secp256k1Signer.swift in Sources */,
|
||||
2F848B66282D5236005D3176 /* PbesJwe.swift in Sources */,
|
||||
55ADE28324F5412600D9990E /* TokenSigning.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -15,9 +15,9 @@ public struct KeyId: VCCryptoSecret {
|
|||
self.id = id
|
||||
}
|
||||
|
||||
public func isValidKey() -> Bool {
|
||||
return true
|
||||
}
|
||||
public func isValidKey() throws { }
|
||||
|
||||
public func migrateKey(fromAccessGroup oldAccessGroup: String?) throws { }
|
||||
|
||||
public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> Void) throws { }
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -14,13 +14,13 @@ struct MockVCCryptoSecret: VCCryptoSecret {
|
|||
|
||||
let accessGroup: String? = nil
|
||||
|
||||
func isValidKey() -> Bool {
|
||||
true
|
||||
}
|
||||
func isValidKey() throws { }
|
||||
|
||||
func migrateKey(fromAccessGroup oldAccessGroup: String?) throws {}
|
||||
|
||||
let id: UUID
|
||||
|
||||
func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> Void) throws { }
|
||||
}
|
||||
|
||||
struct MockAlgorithm: Signing {
|
||||
|
|
Загрузка…
Ссылка в новой задаче